Skip to main content
ClickHouse permet de créer des fonctions définies par l’utilisateur (UDF) écrites en WebAssembly. Vous pouvez ainsi exécuter une logique personnalisée écrite dans des langages comme Rust, C, C++ ou d’autres, après les avoir compilés en modules WebAssembly.

Vue d’ensemble

Un module WebAssembly est un fichier binaire compilé qui contient une ou plusieurs fonctions pouvant être appelées depuis ClickHouse. Considérez un module comme une bibliothèque ou un objet partagé que vous chargez une seule fois et réutilisez à de multiples reprises. Les modules WebAssembly contenant des UDF peuvent être écrits dans n’importe quel langage pouvant être compilé en WebAssembly, comme Rust, C ou C++. Le code compilé en WebAssembly (code « invité ») et exécuté par ClickHouse (« host ») s’exécute dans un environnement isolé de type sandbox, avec accès uniquement à un espace mémoire dédié. Le code invité exporte des fonctions que ClickHouse peut invoquer ; cela inclut les fonctions qui implémentent votre logique personnalisée (utilisée pour définir des UDF), ainsi que les fonctions utilitaires nécessaires à la gestion de la mémoire et à l’échange de données entre ClickHouse et le code WebAssembly. Votre code doit être compilé en WebAssembly « autonome » (alias wasm32-unknown-unknown), sans dépendre d’un système d’exploitation ni d’une bibliothèque standard. En outre, seule la cible WebAssembly 32 bits par défaut est prise en charge (pas d’extension wasm64). Le module doit respecter l’un des protocoles de communication (ABI) pris en charge pour interagir avec ClickHouse. Une fois compilé, le code binaire du module est chargé dans ClickHouse en l’insérant dans la table system.webassembly_modules. Après cela, vous pouvez créer des UDF qui référencent les fonctions exportées par le module à l’aide de l’instruction CREATE FUNCTION ... LANGUAGE WASM.

Prérequis

Activez la prise en charge de WebAssembly dans la configuration de ClickHouse :
Implémentations du moteur disponibles :
  • wasmtime (par défaut, recommandé) — utilise WasmTime
  • wasmedge — utilise WasmEdge

Quick Start

Cet exemple illustre le workflow complet de création d’une UDF WebAssembly en implémentant un calculateur de la conjecture de Collatz. Nous écrirons le code au format texte WebAssembly (WAT), qui est une représentation lisible par l’humain de WebAssembly ; aucun langage de programmation n’est donc nécessaire à ce stade. ClickHouse exige que le module soit au format binaire ; nous utiliserons donc un transpileur pour convertir le WAT en WASM. Pour effectuer cette conversion, vous pouvez utiliser wat2wasm du WebAssembly Binary Toolkit (WABT) ou la commande parse de wasm-tools.
Dans l’extrait ci-dessus, nous envoyons directement le code binaire WASM au clickhouse client à l’aide de FORMAT RawBlob afin de l’insérer dans la table system.webassembly_modules. Nous définissons ensuite l’UDF qui fait référence à la fonction steps exportée par le module :
Notez que nous indiquons le nom de la fonction du module après ::, car il diffère du nom de l’UDF. Nous pouvons maintenant utiliser la fonction collatz_steps dans nos requêtes :
La colonne number est explicitement convertie en UInt32, car les fonctions WebAssembly attendent une correspondance exacte avec les types spécifiés dans la signature de l’instruction CREATE FUNCTION. Le résultat obtenu est la séquence des étapes de Collatz pour les nombres de 1 à 100, correspondant à la suite A006577 de l’OEIS.

Gérer les modules WASM via la table système

Les modules WebAssembly sont stockés dans la table system.webassembly_modules, qui a la structure suivante :
  • Colonnes
    • name String — Nom du module. Non vide, caractères de mot uniquement.
    • code String — Code WASM binaire brut. En écriture seule ; les lectures renvoient une chaîne vide.
    • hash UInt256 — SHA256 du binaire du module (zéro s’il est présent sur le disk mais pas encore chargé).
La gestion des modules s’effectue via les opérations SQL standard sur cette table :

Insérer un module

Si nécessaire, fournissez le hash d’intégrité :
Si le hash fourni ne correspond pas au SHA256 calculé pour le code du module, l’insertion échoue. Cela peut être utile lors du chargement de modules depuis des sources externes telles que S3 ou HTTP.

Distribuer un module à l’échelle d’un cluster

system.webassembly_modules est une table propre à chaque instance — un INSERT n’atteint que la réplique qui gère la connection. Il n’existe pas de forme ON CLUSTER pour l’instruction INSERT, donc un CREATE FUNCTION ... ON CLUSTER exécuté ensuite échouera sur les répliques qui ne disposent pas du module :
Pour répliquer un insert sur tous les nœuds, écrivez dans la fonction de table cluster plutôt que dans la table locale system.webassembly_modules :
Cette approche repose sur le fait que le chemin sous-jacent d’écriture distribuée passe par chaque réplique au sein de chaque shard, ce qui ne se produit que lorsque le cluster est configuré avec internal_replication=false. Avec internal_replication=true (la valeur par défaut pour les clusters qui utilisent ReplicatedMergeTree pour gérer eux-mêmes la réplication), l’insert est envoyé à une seule réplique saine par shard, et system.webassembly_modules n’est pas répliquée par ce chemin ; certaines répliques n’auront donc toujours pas le module. Dans cette configuration, vous devez effectuer un insert sur chaque réplique individuellement, par exemple en parcourant system.clusters et en écrivant via remote(...) pour chaque hôte, ou en copiant le binaire dans user_scripts/wasm/ sur chaque hôte.Vous pouvez vérifier internal_replication pour un cluster avec SELECT cluster, shard_num, internal_replication FROM system.clusters.
Après l’insert diffusé, le module est présent sur chaque réplique et CREATE FUNCTION ... ON CLUSTER aboutit :
Vous pouvez vérifier que le module est chargé sur toutes les répliques avec clusterAllReplicas :
Les insertions dans system.webassembly_modules sont idempotentes pour une même paire (name, hash) ; il est donc sans risque de réexécuter l’insert distribué, et c’est un moyen raisonnable de rétablir l’état après le remplacement d’une réplique. Notez que les serveurs nouvellement ajoutés ne reçoivent pas rétroactivement les modules existants — vous devez réexécuter l’insert sur le cluster mis à jour, ou placer le binaire dans le répertoire user_scripts/wasm/ sur le nouvel hôte.

Lister les modules

Supprimer un module

La suppression s’effectue à l’aide de l’instruction DELETE FROM system.webassembly_modules WHERE name = '...'. Le prédicat doit être soit name = 'literal' pour une correspondance exacte, soit name LIKE 'pattern' pour supprimer tous les modules dont le nom correspond au motif ; aucune autre forme n’est acceptée.
Si des UDFs existantes font référence à l’un des modules correspondants, la suppression échoue ; vous devez donc d’abord supprimer ces UDFs.

Créer une UDF WebAssembly

Syntaxe :
Paramètres :
  • function_name : Nom de la fonction dans ClickHouse. Peut être différent du nom de la fonction exportée par le module.
  • FROM 'module_name' :: 'source_function_name' : Nom du module WASM chargé et nom de la fonction du module WASM à utiliser (par défaut : function_name)
  • ARGUMENTS : Liste des noms et types d’arguments (les noms sont facultatifs et utilisés pour les formats de sérialisation qui prennent en charge les champs nommés)
  • ABI : Version de l’interface binaire d’application
    • ROW_DIRECT : Correspondance de types directe, traitement ligne par ligne
    • BUFFERED_V1 : Traitement par blocs avec sérialisation
    • ASSEMBLYSCRIPT : Traitement ligne par ligne pour les modules produits par le compilateur AssemblyScript. Les types numériques sont mappés vers les primitives d’AssemblyScript ; le String de ClickHouse est mappé vers le string d’AssemblyScript.
  • DETERMINISTIC : Déclare la fonction comme déterministe — elle renvoie toujours le même résultat pour la même entrée. Lorsqu’elle est spécifiée, ClickHouse peut précalculer les appels dont tous les arguments sont des constantes : la fonction est évaluée une seule fois lors de l’analyse de la requête, puis le résultat est réutilisé pour chaque ligne.
  • SHA256_HASH : Hash attendu du module pour vérification (rempli automatiquement s’il est omis) ; peut être utilisé pour garantir que le bon module WASM est chargé sur différentes répliques.
  • SETTINGS : Paramètres propres à la fonction
    • serialization_format String — Format utilisé pour sérialiser les blocs d’arguments transmis au module et analyser le résultat renvoyé. Utilisé uniquement par ABI BUFFERED_V1. Valeurs prises en charge : MsgPack, JSONEachRow, CSV, TSV, TSVRaw, RowBinary et Buffers. Valeur par défaut : MsgPack. Les formats par blocs tels que Buffers doivent renvoyer une seule colonne dont le type correspond à la signature de fonction déclarée.
    • webassembly_udf_enable_fuel Bool — Active un budget de fuel limité pour la fonction. Valeur par défaut : true. Lorsque false, le paramètre de requête webassembly_udf_max_fuel est ignoré pour cette fonction. La désactivation des limites de fuel peut améliorer les performances lors de l’utilisation du moteur wasmtime. Cependant, pour du code invité non fiable ou bogué, cela peut augmenter le risque d’exécution incontrôlée.

Versions d’ABI

Pour interagir avec ClickHouse, les modules WebAssembly doivent respecter l’une des ABI (interfaces binaires d’application) prises en charge.
  • ROW_DIRECT : correspondance directe des types (types primitifs Int32, UInt32, Int64, UInt64, Float32, Float64 uniquement)
  • BUFFERED_V1 : types complexes avec sérialisation
  • ASSEMBLYSCRIPT : interopération ligne par ligne avec les modules AssemblyScript ; prend en charge les types numériques et String.

ABI ROW_DIRECT

Appelle directement une fonction WASM exportée pour chaque ligne.
  • Les arguments et les types de retour doivent être des types numériques Int32/UInt32/Int64/UInt64/Float32/Float64/Int128/UInt128.
  • Les chaînes de caractères ne sont pas prises en charge par cette ABI.
  • Les signatures doivent correspondre à l’export WASM (i32/i64/f32/f64/v128).
  • Le module n’a pas besoin d’exporter de fonctions de support.
Par exemple, pour une fonction ayant la signature :
Peut être créé ainsi :
WebAssembly ne distingue pas les arguments signés des arguments non signés, mais utilise plutôt des instructions différentes pour interpréter les valeurs. La taille de l’argument doit donc correspondre exactement, tandis que le caractère signé ou non est déterminé par les opérations à l’intérieur de la fonction.

ABI BUFFERED_V1

Cette ABI est expérimentale et est susceptible d’évoluer dans les prochaines versions.
Traite des blocs entiers en une seule fois via une (dé)sérialisation dans la mémoire WASM. Prend en charge tous les types d’arguments et de retour. Les données sont échangées via des buffers dans la mémoire WASM. Un buffer est une structure de 8 octets contenant un pointeur vers les données et leur taille (deux valeurs u32 au format little-endian). Les buffers sont transmis par handle — un pointeur vers cette structure, et non vers les données elles-mêmes. Le code invité doit exporter deux fonctions permettant de créer et de détruire ces buffers. Pour chaque bloc d’entrée, ClickHouse :
  1. Sérialise les colonnes d’arguments à l’aide du serialization_format de la fonction (MsgPack par défaut). Les formats basés sur les lignes écrivent les valeurs ligne par ligne, avec les valeurs d’arguments dans l’ordre de leur déclaration dans ARGUMENTS ; les formats comportant des champs nommés utilisent les noms des arguments. Déclarez donc ces derniers dans ARGUMENTS lorsque vous utilisez de tels formats.
  2. Appelle clickhouse_create_buffer, exportée par le module, et copie les données sérialisées dans la mémoire pointée par le buffer renvoyé.
  3. Appelle la fonction définie par l’utilisateur avec deux arguments i32 : le handle du buffer d’entrée (0 si la fonction n’a aucun argument) et le nombre de lignes. La fonction renvoie un unique i32 — le handle du buffer de résultat, que le code invité alloue lui-même ; renvoyer 0 fait échouer la requête avec une erreur.
  4. Lit le buffer de résultat : il doit contenir exactement une colonne avec exactement le même nombre de lignes, sérialisée dans le même format. Pour les formats comportant des champs nommés, tels que JSONEachRow, la colonne de résultat doit être nommée result.
  5. Appelle clickhouse_destroy_buffer sur les handles du buffer d’entrée (le cas échéant) et du buffer de résultat. Le code invité ne doit pas libérer lui-même le buffer de résultat ni renvoyer le handle d’entrée comme résultat — il serait alors détruit deux fois.
La fonction est invoquée une fois par bloc d’entrée : une requête volumineuse est divisée en plusieurs blocs par le pipeline de requête (le nombre maximal de lignes par appel peut également être limité par le paramètre webassembly_udf_max_input_block_size). Le nombre de lignes transmis à chaque appel correspond donc à la taille de ce bloc, et non à celle de la requête entière.
Les octets exacts contenus dans les buffers sont définis par le format choisi, et le code invité doit les décoder et les encoder lui-même. En particulier, pour les valeurs String :
  • MsgPack : ClickHouse écrit les valeurs String à l’aide de la famille de types bin (0xc4/0xc5/0xc6), et non de la famille str. Les deux familles sont acceptées lors de l’analyse du résultat.
  • RowBinary : chaque String est préfixée de sa longueur en octets sous forme de varint non signé (LEB128), et non d’un entier à largeur fixe.
  • JSONEachRow : chaque ligne de résultat doit être un objet avec la clé result, par exemple {"result":"value"}.
Exemple complet en C freestanding : str_reverse inverse les octets de chaque chaîne d’entrée en utilisant serialization_format = 'RowBinary'. Les instances de module sont réutilisées d’un bloc à l’autre ; clickhouse_destroy_buffer doit donc réellement libérer la mémoire. Ici, l’allocator se réinitialise une fois tous les buffers détruits.
Compilez avec clang et wasm-ld (fourni avec LLVM/lld) :
Les fonctions doivent être visibles parmi les exports du module : marquez-les avec __attribute__((export_name("..."))) comme ci-dessus, ou établissez l’édition de liens avec wasm-ld --export-all. -fno-builtin empêche clang de convertir les simples boucles sur les octets en appels à memcpy/memset, qui ne sont pas disponibles sans bibliothèque standard. Chargez le module et créez la fonction :

ABI ASSEMBLYSCRIPT

Cible les modules produits par le compilateur AssemblyScript. Chaque ligne déclenche un appel à la fonction exportée, en convertissant les valeurs ClickHouse en types primitifs AssemblyScript et en objets string. Types pris en charge :
  • Numériques : Int8/UInt8, Int16/UInt16 (élargis en i32 à l’interface), Int32/UInt32, Int64/UInt64, Float32, Float64
  • String — correspond à string en AssemblyScript (UTF-16 dans la mémoire WASM). ClickHouse gère automatiquement la conversion UTF-8 ↔ UTF-16.
  • Les classes AssemblyScript personnalisées ne sont pas prises en charge comme types d’argument ou de retour — leurs identifiants de classe à l’exécution ne sont pas stables d’une compilation à l’autre (voir AssemblyScript#2982).
Exigences du module : Le module doit être compilé avec le runtime géré d’AssemblyScript afin que __new, __pin et __unpin soient exportés. La gestion standard des chaînes entrantes/sortantes s’appuie sur eux. Invocation recommandée :
AssemblyScript importe également env.abort pour les interruptions d’exécution (mémoire insuffisante, vérification des limites, etc.). ClickHouse fournit automatiquement cet import : lorsqu’un abort est déclenché, la requête en cours échoue avec une exception WASM_ERROR qui inclut le message AssemblyScript décodé et l’emplacement dans le code source. Exemple:
Après compilation avec asc et chargement du fichier .wasm obtenu dans system.webassembly_modules, déclarez les UDFs comme suit :

Remarque sur le développement de UDFs en Rust

Pour les programmes Rust, nous fournissons une crate utilitaire clickhouse-wasm-udf afin de simplifier le développement de WebAssembly UDFs pour ClickHouse. Cette crate fournit des fonctions de gestion de la mémoire. Vous n’avez donc pas besoin d’implémenter manuellement les fonctions clickhouse_create_buffer et clickhouse_destroy_buffer : il vous suffit d’ajouter la crate comme dépendance. Des macros #[clickhouse_wasm_udf] sont également disponibles pour encapsuler vos fonctions Rust habituelles dans le format ABI requis. Avec cette crate, vous pouvez écrire des UDFs comme ceci :
Les macros généreront une fonction wrapper qui accepte et renvoie des structures de buffer, et gérera automatiquement la sérialisation/désérialisation à l’aide de serde.

API hôte disponible pour les modules

Les fonctions hôte suivantes peuvent être importées et utilisées par les modules :
  • clickhouse_server_version() -> i64 — renvoie la version du serveur ClickHouse sous forme d’entier (p. ex. 25011001 pour v25.11.1.1).
  • clickhouse_throw(ptr: i32, size: i32) — lève une erreur avec le message fourni. Accepte un pointeur vers l’emplacement mémoire contenant la chaîne du message d’erreur ainsi que la taille de cette chaîne.
  • clickhouse_log(ptr: i32, size: i32) — écrit un message dans le journal texte du serveur ClickHouse.
  • clickhouse_random(ptr: i32, size: i32) — remplit la mémoire avec des octets aléatoires.
  • env.abort(message: i32, fileName: i32, line: i32, column: i32) — fournie pour les modules compatibles avec AssemblyScript. Son appel (ou le déclenchement d’un trap du runtime AssemblyScript qui l’appelle) met fin à l’UDF avec une exception WASM_ERROR contenant le message décodé et l’emplacement dans le code source. Les modules qui n’importent pas env.abort ne sont pas affectés.

Paramètres

Les paramètres suivants, au niveau de la requête, contrôlent l’exécution des UDF WebAssembly :
  • webassembly_udf_max_fuel — Limite de fuel par exécution d’une instance d’UDF WebAssembly. Chaque instruction WebAssembly consomme une certaine quantité de fuel. La valeur est multipliée par 1024 avant d’être transmise au runtime, ainsi webassembly_udf_max_fuel = 1 correspond à environ 1024 unités de fuel. Définissez-la sur 0 pour supprimer toute limite finie. S’applique uniquement aux fonctions dont le paramètre par fonction webassembly_udf_enable_fuel est true, ce qui est la valeur par défaut.
  • webassembly_udf_max_memory — Limite de mémoire, en octets, par instance d’UDF WebAssembly.
  • webassembly_udf_max_input_block_size — Nombre maximal de lignes transmises à une UDF WebAssembly dans un seul bloc. Définissez 0 pour traiter toutes les lignes en une seule fois.
  • webassembly_udf_max_instances — Nombre maximal d’instances d’UDF WebAssembly pouvant s’exécuter en parallèle par fonction.
Exemple d’utilisation :

Voir aussi

Dernière modification le 18 août 2026