O ClickHouse oferece suporte à criação de funções definidas pelo usuário (UDFs) escritas em WebAssembly. Isso permite executar lógica personalizada escrita em linguagens como Rust, C, C++ ou outras, ao compilá-las em módulos WebAssembly.
Sem suporte no ClickHouse Cloud Recurso experimentalVisão geral
Um módulo WebAssembly é um arquivo binário compilado que contém uma ou mais funções que podem ser chamadas pelo ClickHouse. Pense em um módulo como uma biblioteca ou objeto compartilhado que você carrega uma vez e reutiliza muitas vezes.
Um módulo WebAssembly que contém UDFs pode ser escrito em qualquer linguagem que possa ser compilada para WebAssembly, como Rust, C ou C++.
O código compilado para WebAssembly (código "guest") e executado pelo ClickHouse ("host") roda em um ambiente isolado, com acesso apenas a um espaço de memória dedicado.
O código guest exporta funções que o ClickHouse pode invocar — incluindo as funções que implementam sua lógica personalizada (usadas para definir UDFs), bem como funções de suporte necessárias para o gerenciamento de memória e a troca de dados entre o ClickHouse e o código WebAssembly.
Seu código deve ser compilado para WebAssembly "freestanding" (também conhecido como wasm32-unknown-unknown), sem nenhuma dependência de sistema operacional ou biblioteca padrão. Além disso, apenas o alvo padrão de WebAssembly de 32 bits é compatível (sem a extensão wasm64).
O módulo deve seguir um dos protocolos de comunicação (ABIs) compatíveis para interagir com o ClickHouse.
Depois de compilado, o código binário do módulo é carregado no ClickHouse por meio da inserção na tabela system.webassembly_modules.
Depois disso, você pode criar UDFs que fazem referência a funções exportadas pelo módulo usando a instrução CREATE FUNCTION ... LANGUAGE WASM.
Pré-requisitos
Ative o suporte a WebAssembly na configuração do ClickHouse:
<clickhouse>
<allow_experimental_webassembly_udf>true</allow_experimental_webassembly_udf>
<webassembly_udf_engine>wasmtime</webassembly_udf_engine>
</clickhouse>Implementações de engine disponíveis:
wasmtime(padrão e, atualmente, a única) — usa Wasmtime
Início rápido
Este exemplo demonstra o fluxo completo de criação de uma WebAssembly UDF, implementando uma calculadora da conjectura de Collatz.
Vamos escrever o código no formato WebAssembly Text (WAT), que é uma representação legível do WebAssembly, então não é necessário usar nenhuma linguagem de programação nesta etapa.
O ClickHouse exige que o módulo esteja em formato binário, por isso usaremos um transpilador para converter WAT em WASM.
Para realizar essa conversão, você pode usar o wat2wasm, do WebAssembly Binary Toolkit (WABT), ou o comando parse, do wasm-tools.
cat << 'EOF' | wasm-tools parse | clickhouse client -q "INSERT INTO system.webassembly_modules (name, code) SELECT 'collatz', code FROM input('code String') FORMAT RawBlob"
(module
(func $next (param $n i32) (result i32)
local.get $n i32.const 1 i32.and
(if (result i32)
(then local.get $n i32.const 3 i32.mul i32.const 1 i32.add)
(else local.get $n i32.const 2 i32.div_u)))
(func $steps (export "steps") (param $n i32) (result i32)
(local $count i32)
local.get $n i32.const 1 i32.lt_u
(if (then i32.const 0 return))
(block $done (loop $loop
local.get $n i32.const 1 i32.eq br_if $done
local.get $n call $next local.set $n
local.get $count i32.const 1 i32.add local.set $count
br $loop))
local.get $count)
)
EOFNo trecho acima, enviamos o código binário WASM diretamente para o ClickHouse client usando FORMAT RawBlob para inseri-lo na tabela system.webassembly_modules.
Em seguida, definimos a UDF que faz referência à função steps exportada pelo módulo:
CREATE FUNCTION collatz_steps LANGUAGE WASM ARGUMENTS (n UInt32) RETURNS UInt32 FROM 'collatz' :: 'steps';Observe que especificamos o nome da função no módulo após ::, pois ele difere do nome da UDF.
Agora podemos usar a função collatz_steps em nossas consultas:
SELECT groupArray(collatz_steps(number :: UInt32))
FROM numbers(1, 100)
FORMAT TSVA coluna number é convertida explicitamente para UInt32, porque as funções WebAssembly exigem uma correspondência exata de tipos com a assinatura especificada na instrução CREATE FUNCTION.
No resultado, obtivemos a sequência de etapas de Collatz para números de 1 a 100, correspondente à sequência A006577 da OEIS.
[0,1,7,2,5,8,16,3,19,6,14,9,9,17,17,4,12,20,20,7,7,15,15,10,23,10,111,18,18,18,106,5,26,13,13,21,21,21,34,8,109,8,29,16,16,16,104,11,24,24,24,11,11,112,112,19,32,19,32,19,19,107,107,6,27,27,27,14,14,14,102,22,115,22,14,22,22,35,35,9,22,110,110,9,9,30,30,17,30,17,92,17,17,105,105,12,118,25,25,25]Gerencie módulos WASM por meio da tabela de sistema
Os módulos WebAssembly são armazenados na tabela system.webassembly_modules, que tem a seguinte estrutura:
- Colunas
nameString — Nome do módulo. Não pode estar vazio; apenas caracteres de palavra.codeString — Código WASM binário bruto. Somente para escrita; as leituras retornam uma string vazia.hashUInt256 — SHA256 do binário do módulo (zero se estiver presente em disco, mas ainda não tiver sido carregado).
O gerenciamento de módulos é feito por meio de operações SQL padrão nessa tabela:
Inserir um módulo
INSERT INTO system.webassembly_modules (name, code)
SELECT 'my_module', base64Decode('AGFzbQEAAAA...');Opcionalmente, informe o hash de integridade:
INSERT INTO system.webassembly_modules (name, code, hash)
SELECT 'my_module', base64Decode('...'), reinterpretAsUInt256(unhex('369f...c57d'));Se o hash fornecido não corresponder ao SHA256 calculado para o código do módulo, a inserção falhará. Isso pode ser útil ao carregar módulos a partir de fontes externas, como S3 ou HTTP.
Distribuir um módulo em um cluster
system.webassembly_modules é uma tabela por instância — um INSERT é gravado apenas na réplica que atende à conexão. Não existe uma forma ON CLUSTER para a instrução INSERT, portanto, um CREATE FUNCTION ... ON CLUSTER subsequente falhará nas réplicas que não têm o módulo:
Code: 674. DB::Exception: WebAssembly module 'collatz' not found:
while adding user defined function `collatz_steps`. (RESOURCE_NOT_FOUND)Para propagar um insert para todos os nós, grave na função de tabela cluster em vez de na tabela local system.webassembly_modules:
cat collatz.wasm | clickhouse client -q "
INSERT INTO FUNCTION cluster('default', 'system', 'webassembly_modules') (name, code)
SELECT 'collatz', code FROM input('code String') FORMAT RawBlob"Após o insert distribuído, o módulo passa a estar presente em todas as réplicas e CREATE FUNCTION ... ON CLUSTER é executado com sucesso:
CREATE FUNCTION collatz_steps ON CLUSTER 'default'
LANGUAGE WASM FROM 'collatz' :: 'steps'
ARGUMENTS (n UInt32) RETURNS UInt32;Você pode verificar se o módulo está carregado em todo o cluster com clusterAllReplicas:
SELECT hostName(), name FROM clusterAllReplicas('default', system.webassembly_modules) WHERE name = 'collatz';As inserções em system.webassembly_modules são idempotentes para o mesmo par (name, hash), portanto executar novamente a inserção distribuída é seguro e é uma forma razoável de reparar o estado depois que uma réplica for substituída. Observe que servidores adicionados recentemente não recebem módulos existentes retroativamente — você deve executar novamente a inserção no cluster atualizado ou colocar o binário no diretório user_scripts/wasm/ no novo host.
Listar módulos
SELECT name, lower(hex(reinterpretAsFixedString(hash))) AS sha256 FROM system.webassembly_modules
┌Excluir um módulo
A exclusão é feita pela instrução DELETE FROM system.webassembly_modules WHERE name = '...'.
O predicado deve ser name = 'literal' para correspondência exata ou name LIKE 'pattern' para excluir todos os módulos cujo nome corresponda ao padrão; nenhum outro formato é aceito.
DELETE FROM system.webassembly_modules WHERE name = 'collatz';
-- Exclusão em massa de todos os módulos cujo nome começa com `tmp_` (o sublinhado literal é escapado como `\_`):
DELETE FROM system.webassembly_modules WHERE name LIKE 'tmp\_%';Se alguma UDF existente fizer referência a um dos módulos encontrados, a exclusão falhará; portanto, primeiro você deve excluir essas UDFs.
Criar uma WebAssembly UDF
Sintaxe:
CREATE [OR REPLACE] FUNCTION function_name
LANGUAGE WASM
FROM 'module_name' [:: 'source_function_name']
ARGUMENTS ( [name type[, ...]] | [type[, ...]] )
RETURNS return_type
[ABI ROW_DIRECT | ABI BUFFERED_V1 | ABI ASSEMBLYSCRIPT]
[DETERMINISTIC]
[SHA256_HASH 'hex']
[SETTINGS key = value[, ...]];Parâmetros:
function_name: Nome da função no ClickHouse. Pode ser diferente do nome da função exportada no módulo.FROM 'module_name' :: 'source_function_name': Nome do módulo WASM carregado e nome da função no módulo WASM a ser usada (o padrão é function_name)ARGUMENTS: Lista de nomes e tipos de argumentos (os nomes são opcionais e usados em formatos de serialização compatíveis com campos nomeados)ABI: Versão da Application Binary InterfaceROW_DIRECT: Mapeamento direto de tipos, processamento linha por linhaBUFFERED_V1: Processamento baseado em blocos com serializaçãoASSEMBLYSCRIPT: Processamento linha por linha para módulos produzidos pelo compilador AssemblyScript. Tipos numéricos são mapeados para tipos primitivos do AssemblyScript;Stringdo ClickHouse é mapeado parastringdo AssemblyScript.
DETERMINISTIC: Declara a função como determinística — sempre retorna a mesma saída para a mesma entrada. Quando especificado, o ClickHouse pode fazer o constant folding de chamadas em que todos os argumentos são constantes: a função é avaliada uma vez durante a análise da consulta, e o resultado é reutilizado para cada linha.SHA256_HASH: Hash esperado do módulo para verificação (preenchido automaticamente se omitido); pode ser usado para garantir que o módulo WASM correto seja carregado em diferentes réplicas.SETTINGS: Configurações por funçãoserialization_formatString — Formato usado para serializar blocos de argumentos passados ao módulo e analisar o resultado retornado. Usado apenas porABI BUFFERED_V1. Valores compatíveis:MsgPack,JSONEachRow,CSV,TSV,TSVRaw,RowBinaryeBuffers. Padrão:MsgPack. Formatos baseados em blocos, comoBuffers, devem retornar uma única coluna cujo tipo corresponda à assinatura da função declarada.webassembly_udf_enable_fuelBool — Habilita um orçamento finito de fuel para a função. Padrão:true. Quandofalse, a configuração no nível da consultawebassembly_udf_max_fuelé ignorada para esta função. Desabilitar os limites de fuel pode melhorar o desempenho. No entanto, para código guest não confiável ou com bugs, isso pode aumentar o risco de execução descontrolada.
Versões de ABI
Para interagir com o ClickHouse, os módulos WebAssembly devem seguir uma das ABIs (Application Binary Interfaces) compatíveis.
ROW_DIRECT: Mapeamento direto de tipos (somente tipos primitivosInt32,UInt32,Int64,UInt64,Float32,Float64)BUFFERED_V1: Tipos complexos com serializaçãoASSEMBLYSCRIPT: Interoperação linha por linha com módulos AssemblyScript; oferece suporte a tipos numéricos eString.
ABI ROW_DIRECT
Chama diretamente uma função WASM exportada para cada linha.
- Argumentos e tipos de retorno devem ser tipos numéricos
Int32/UInt32/Int64/UInt64/Float32/Float64/Int128/UInt128. - Strings não são compatíveis com esta ABI.
- As assinaturas devem corresponder à exportação WASM (
i32/i64/f32/f64/v128). - O módulo não precisa exportar funções de suporte.
Por exemplo, uma função com a assinatura:
(func (param i32 i64 f32) (result f64) ...)Pode ser criado da seguinte forma:
CREATE FUNCTION my_func ARGUMENTS (Int32, UInt64, Float32) RETURNS Float64 ...O WebAssembly não faz distinção entre argumentos com sinal e sem sinal; em vez disso, usa instruções diferentes para interpretar os valores. Assim, o tamanho do argumento deve corresponder exatamente, enquanto o uso de sinal é determinado pelas operações dentro da função.
ABI BUFFERED_V1
Processa blocos inteiros de uma só vez usando (des)serialização por meio da memória WASM. Compatível com quaisquer tipos de argumentos e de retorno.
Os dados são trocados por meio de buffers na memória WASM. Um buffer é uma estrutura de 8 bytes que contém um ponteiro para os dados e o tamanho deles (dois valores u32 little-endian). Os buffers são passados por identificador — um ponteiro para essa estrutura, e não para os próprios dados. O código guest deve exportar duas funções para criar e destruir esses buffers.
Para cada bloco de entrada, o ClickHouse:
- Serializa as colunas de argumentos usando o
serialization_formatda função (MsgPackpor padrão). Formatos baseados em linhas gravam os valores linha a linha, com os valores dos argumentos na ordem em que são declarados emARGUMENTS; formatos com campos nomeados usam os nomes dos argumentos, portanto, declare-os emARGUMENTSao usar esses formatos. - Chama
clickhouse_create_buffer, exportada pelo módulo, e copia os dados serializados para a memória apontada pelo buffer retornado. - Chama a função definida pelo usuário com dois argumentos
i32: o identificador do buffer de entrada (0se a função não tiver argumentos) e o número de linhas. A função retorna um únicoi32— o identificador do buffer de resultado, que o próprio código guest aloca; retornar0faz a consulta falhar com um erro. - Lê o buffer de resultado: ele deve conter exatamente uma coluna com exatamente o mesmo número de linhas, serializada no mesmo formato. Para formatos com campos nomeados, como
JSONEachRow, a coluna de resultado deve se chamarresult. - Chama
clickhouse_destroy_buffernos identificadores dos buffers de entrada (se houver) e de resultado. O código guest não deve liberar o buffer de resultado por conta própria nem retornar o identificador de entrada como resultado — caso contrário, ele seria destruído duas vezes.
A função é invocada uma vez por bloco de entrada: uma consulta grande é dividida em vários blocos pelo pipeline de consulta (o número máximo de linhas por chamada também pode ser limitado pela configuração webassembly_udf_max_input_block_size). Portanto, a contagem de linhas passada em cada chamada corresponde ao tamanho desse bloco, e não ao da consulta inteira.
(module
;; Allocate a new buffer of specified size
;; Returns: handle to Buffer structure (not direct data pointer!) with pointer to data and size
(func (export "clickhouse_create_buffer")
(param $size i32) ;; Size of data to allocate
(result i32)) ;; Returns buffer handle with enough space
;; Free a buffer by its handle
(func (export "clickhouse_destroy_buffer")
(param $handle i32) ;; Buffer handle to free
(result)) ;; No return value
;; User-defined function
(func (export "user_defined_function1")
(param $input_buffer_handle i32) ;; Input buffer handle
(param $n i32) ;; Number of rows in input
(result i32)) ;; Returns output buffer handle
)Um exemplo completo em C freestanding: str_reverse inverte os bytes de cada string de entrada usando serialization_format = 'RowBinary'. As instâncias do módulo são reutilizadas entre blocos, portanto clickhouse_destroy_buffer precisa realmente liberar a memória — aqui, o allocator é redefinido quando todos os buffers são destruídos.
#include <stdint.h>
typedef struct {
uint8_t * data;
uint32_t size;
} ClickHouseBuffer;
#define HEAP_SIZE (1 << 24)
static _Alignas(16) uint8_t heap[HEAP_SIZE];
static uint32_t heap_pos = 0;
static uint32_t live_buffers = 0;
__attribute__((export_name("clickhouse_create_buffer")))
ClickHouseBuffer * clickhouse_create_buffer(uint32_t size)
{
uint32_t total = (sizeof(ClickHouseBuffer) + size + 15u) & ~15u;
if (heap_pos + total > HEAP_SIZE)
return 0; /* a zero handle makes the host fail the query with an error */
ClickHouseBuffer * buf = (ClickHouseBuffer *) &heap[heap_pos];
buf->data = &heap[heap_pos + sizeof(ClickHouseBuffer)];
buf->size = size;
heap_pos += total;
++live_buffers;
return buf;
}
__attribute__((export_name("clickhouse_destroy_buffer")))
void clickhouse_destroy_buffer(ClickHouseBuffer * buf)
{
if (--live_buffers == 0)
heap_pos = 0;
}
/* RowBinary prefixes each String with its byte length as an unsigned varint (LEB128) */
static uint64_t read_varint(const uint8_t ** pp)
{
uint64_t value = 0;
for (int shift = 0;; shift += 7)
{
uint8_t b = *(*pp)++;
value |= (uint64_t)(b & 0x7f) << shift;
if (!(b & 0x80))
return value;
}
}
static void write_varint(uint8_t ** pp, uint64_t value)
{
while (value >= 0x80)
{
*(*pp)++ = (uint8_t)(value | 0x80);
value >>= 7;
}
*(*pp)++ = (uint8_t)value;
}
/* Reverses the bytes of each input string */
__attribute__((export_name("str_reverse")))
ClickHouseBuffer * str_reverse(ClickHouseBuffer * input, uint32_t num_rows)
{
ClickHouseBuffer * out = clickhouse_create_buffer(input->size);
if (!out)
return 0;
const uint8_t * in = input->data;
uint8_t * o = out->data;
for (uint32_t row = 0; row < num_rows; ++row)
{
uint64_t len = read_varint(&in);
write_varint(&o, len);
for (uint64_t i = 0; i < len; ++i)
o[i] = in[len - 1 - i];
in += len;
o += len;
}
out->size = (uint32_t)(o - out->data);
return out;
}Compile usando clang e wasm-ld (incluídos no LLVM/lld):
clang --target=wasm32 -ffreestanding -nostdlib -fno-builtin -c str_reverse.c
wasm-ld --no-entry str_reverse.o -o str_reverse.wasmAs funções devem estar visíveis nas exportações do módulo: marque-as com __attribute__((export_name("..."))), como acima, ou faça a vinculação com wasm-ld --export-all. -fno-builtin impede que o clang converta loops simples de bytes em chamadas a memcpy/memset, que não estão disponíveis sem uma biblioteca padrão.
Carregue o módulo e crie a função:
cat str_reverse.wasm | clickhouse client -q "INSERT INTO system.webassembly_modules (name, code) SELECT 'str_reverse', code FROM input('code String') FORMAT RawBlob"CREATE FUNCTION str_reverse LANGUAGE WASM ABI BUFFERED_V1
FROM 'str_reverse' :: 'str_reverse'
ARGUMENTS (s String) RETURNS String
SETTINGS serialization_format = 'RowBinary';
SELECT str_reverse(toString(number + 100)) FROM numbers(3);001
101
201ABI ASSEMBLYSCRIPT
Destina-se a módulos produzidos pelo compilador AssemblyScript. Cada linha aciona uma chamada para a função exportada, mapeando valores do ClickHouse para tipos primitivos do AssemblyScript e objetos string.
Tipos compatíveis:
-
Numéricos:
Int8/UInt8,Int16/UInt16(ampliados parai32no limite),Int32/UInt32,Int64/UInt64,Float32,Float64 -
String— corresponde astringdo AssemblyScript (UTF-16 na memória WASM). O ClickHouse lida automaticamente com a conversão UTF-8 ↔ UTF-16. -
Classes personalizadas do AssemblyScript não são compatíveis como tipos de argumento ou de retorno — seus IDs de classe do runtime não são estáveis entre compilações (consulte AssemblyScript#2982).
Requisitos do módulo:
O módulo deve ser compilado com o runtime gerenciado do AssemblyScript para que __new, __pin e __unpin sejam exportados. O tratamento padrão de strings de entrada e saída pressupõe isso. A invocação recomendada:
asc src.ts --runtime incremental --exportRuntime -o src.wasmO AssemblyScript também importa env.abort para traps de runtime (falta de memória, verificações de limites etc.). O ClickHouse fornece essa importação automaticamente: quando um abort é disparado, a consulta ativa falha com uma exceção WASM_ERROR que inclui a mensagem decodificada do AssemblyScript e o local no código-fonte.
Exemplo:
// src.ts
export function add(a: u32, b: u32): u32 {
return a + b;
}
export function greet(name: string): string {
return "Hello, " + name + "!";
}Após compilar com asc e carregar o .wasm resultante em system.webassembly_modules, declare as UDFs da seguinte forma:
CREATE FUNCTION as_add
LANGUAGE WASM ABI ASSEMBLYSCRIPT
FROM 'as_example' :: 'add'
ARGUMENTS (a UInt32, b UInt32) RETURNS UInt32;
CREATE FUNCTION as_greet
LANGUAGE WASM ABI ASSEMBLYSCRIPT
FROM 'as_example' :: 'greet'
ARGUMENTS (name String) RETURNS String;Observação sobre o desenvolvimento de UDFs em Rust
Para programas em Rust, fornecemos um crate auxiliar clickhouse-wasm-udf para simplificar o desenvolvimento de WebAssembly UDFs para o ClickHouse. O crate fornece funções para gerenciamento de memória, então você não precisa implementar manualmente as funções clickhouse_create_buffer e clickhouse_destroy_buffer; basta adicionar o crate como dependência. Também há macros #[clickhouse_wasm_udf] para encapsular suas funções Rust comuns no formato ABI exigido.
Com o crate, você pode escrever UDFs assim:
use clickhouse_wasm_udf_bindgen::clickhouse_udf;
#[clickhouse_udf]
pub fn some_udf(data: String) -> HashMap<String, String> {
// Sua implementação aqui
}Macros geram uma função wrapper que aceita e retorna estruturas de buffer e lida automaticamente com a serialização/desserialização usando serde.
API do host disponível para módulos
As funções de host a seguir podem ser importadas e usadas por módulos:
clickhouse_server_version() -> i64— retorna a versão do ClickHouse server como um inteiro (por exemplo, 25011001 para v25.11.1.1).clickhouse_throw(ptr: i32, size: i32)— gera um erro com a mensagem fornecida. Aceita um ponteiro para a posição de memória que contém a string da mensagem de erro e o tamanho da string.clickhouse_log(ptr: i32, size: i32)— registra uma mensagem no log de texto do ClickHouse server.clickhouse_random(ptr: i32, size: i32)— preenche a memória com bytes aleatórios.env.abort(message: i32, fileName: i32, line: i32, column: i32)— fornecido para módulos compatíveis com AssemblyScript. Chamá-lo (ou disparar uma trap de runtime do AssemblyScript que o chame) encerra a UDF com uma exceçãoWASM_ERRORcontendo a mensagem decodificada e a localização no código-fonte. Módulos que não importamenv.abortnão são afetados.
Configurações
As configurações a seguir, no nível da consulta, controlam a execução de UDFs em WebAssembly:
-
webassembly_udf_max_fuel— Limite de fuel por execução de uma instância de UDF em WebAssembly. Cada instrução de WebAssembly consome uma certa quantidade de fuel. O valor é escalado por 1024 antes de ser passado ao runtime, portantowebassembly_udf_max_fuel = 1corresponde a aproximadamente 1024 unidades de fuel. Defina como 0 para não haver limite finito. Aplica-se apenas a funções cuja configuração por funçãowebassembly_udf_enable_fuelsejatrue, que é o padrão. -
webassembly_udf_max_memory— Limite de memória, em bytes, por instância de UDF em WebAssembly. -
webassembly_udf_max_input_block_size— Número máximo de linhas passadas para uma UDF em WebAssembly em um único bloco. Defina como 0 para processar todas as linhas de uma só vez. -
webassembly_udf_max_instances— Número máximo de instâncias de UDF em WebAssembly que podem ser executadas em paralelo por função.
Exemplo de uso:
SET webassembly_udf_max_fuel = 200000;
SELECT my_wasm_udf(column) FROM table;