Managing ClickHouse WASM Modules and WASM UDFs with Atlas
ClickHouse can run user-defined functions (UDFs) backed by WebAssembly (WASM): you compile a function to a WASM binary, load it into the server as a WASM module, and call it from SQL like any built-in function.
In Atlas, WASM modules and WASM UDFs are represented as two related resources. A
wasm_module defines the WASM binary to deploy and its hash, while a
function with lang = WASM references the module and specifies the exported function
to invoke. In this guide, you'll define both resources in HCL and let Atlas generate and apply the migrations.
Prerequisites
- Docker
- Atlas installed on your machine (installation guide)
- An Atlas Pro account (run
atlas loginto authenticate)
Project Setup
ClickHouse introduced support for WASM modules and WASM UDFs in version 26.3, and some WASM UDF options became available in later versions. This example uses ClickHouse version 26.6.
Enabling WASM
ClickHouse requires WebAssembly support to be enabled at the server level. Create a file called config.xml in your
project directory:
<clickhouse>
<allow_experimental_webassembly_udf>true</allow_experimental_webassembly_udf>
<webassembly_udf_engine>wasmtime</webassembly_udf_engine>
</clickhouse>
Starting ClickHouse with Docker
Start a ClickHouse database using your credentials, mounting the configuration file into the server:
docker run -d \
--name clickhouse_wasm \
--hostname clickhouse_wasm \
-p 127.0.0.1:8128:8123 \
-p 127.0.0.1:9008:9000 \
-v ./config.xml:/etc/clickhouse-server/config.d/config.xml:ro \
-e CLICKHOUSE_USER=default \
-e CLICKHOUSE_PASSWORD=your_password \
-e CLICKHOUSE_DB=default \
clickhouse/clickhouse-server:26.6
Configuring the Atlas Project
Create an atlas.hcl project file. Use the volumes attribute to mount the configuration into the dev database
container. WASM support is opt-in, so enable wasm_modules = true in the mode "clickhouse" block:
docker "clickhouse" "dev" {
image = "clickhouse/clickhouse-server:26.6"
volumes = ["./config.xml:/etc/clickhouse-server/config.d/config.xml"]
}
env "local" {
dev = docker.clickhouse.dev.url
migration {
dir = "file://migrations"
}
schema {
src = "file://schema/main.hcl"
mode "clickhouse" {
wasm_modules = true
}
}
}
Defining the Desired State
Defining a WASM Module
The wasm_module resource manages ClickHouse WASM modules. ClickHouse stores modules as
binary WASM, so the binary is supplied as base64 or hexadecimal through expressions such as base64Decode(...) or
unhex(...).
code: Expression returning the raw WASM binary from base64 or hexadecimal input. Write-only; never emitted during inspection.hash: Expression evaluating to the expectedUInt256SHA-256 hash of the module binary. Auto-filled if omitted.
wasm_module "math_module" {
code = sql("unhex('0061736d0100000001070160027f7f017f030201000707010373756200000a09010700200020016b0b')")
hash = sql("reinterpretAsUInt256(unhex('a957fcb890da31e0e4a72bbdb0e04c50ba7ff5cb6946b21d674f9dcb6e7cd488'))")
}
Defining a WASM Function
Functions can be declared with lang = WASM and reference a previously declared wasm_module.
lang: Must beWASMreturns: Function return typemodule: Reference to awasm_moduleresourcesource: Name of the exported function inside the WASM moduleabi: Application Binary Interface version (ROW_DIRECT,BUFFERED_V1, orASSEMBLYSCRIPT)deterministic: Declares whether or not the function is deterministichash: Expected SHA-256 hash of the referenced module. Auto-filled if omitted.settings: Per-function ClickHouse settingsarg: Declares a function argument with a type and an optional name
function "wasm_numbers" {
lang = WASM
returns = UInt32
module = wasm_module.math_module
source = "sub"
abi = ROW_DIRECT
deterministic = true
hash = "a957fcb890da31e0e4a72bbdb0e04c50ba7ff5cb6946b21d674f9dcb6e7cd488"
settings = {
serialization_format = "MsgPack"
webassembly_udf_enable_fuel = 1
}
arg "a" {
type = UInt32
}
arg "b" {
type = UInt32
}
}
Generating Migration Files
Use atlas migrate diff to generate migration files:
atlas migrate diff --env local
This generates a migration file to create the module and function:
-- Create wasm module "math_module"
INSERT INTO system.webassembly_modules (name, code, hash) SELECT 'math_module', unhex('0061736d0100000001070160027f7f017f030201000707010373756200000a09010700200020016b0b'), reinterpretAsUInt256(unhex('a957fcb890da31e0e4a72bbdb0e04c50ba7ff5cb6946b21d674f9dcb6e7cd488'));
-- Create "wasm_numbers" function
CREATE FUNCTION `wasm_numbers`
LANGUAGE WASM
ARGUMENTS (a UInt32, b UInt32)
RETURNS UInt32
FROM 'math_module' :: 'sub'
ABI ROW_DIRECT
DETERMINISTIC
SHA256_HASH 'a957fcb890da31e0e4a72bbdb0e04c50ba7ff5cb6946b21d674f9dcb6e7cd488'
SETTINGS serialization_format = 'MsgPack', webassembly_udf_enable_fuel = 1;
Applying Changes
Run atlas migrate apply to execute the migration against the live database:
atlas migrate apply --env local
Atlas applies the pending migration, creating the WASM module and the WASM user defined function:
Migrating to version 20260731134326 (1 migrations in total):
-- migrating version 20260731134326
-> INSERT INTO system.webassembly_modules (name, code, hash) SELECT 'math_module', unhex('0061736d0100000001070160027f7f017f030201000707010373756200000a09010700200020016b0b'), reinterpretAsUInt256(unhex('a957fcb890da31e0e4a72bbdb0e04c50ba7ff5cb6946b21d674f9dcb6e7cd488'));
-> CREATE FUNCTION `wasm_numbers`
LANGUAGE WASM
ARGUMENTS (a UInt32, b UInt32)
RETURNS UInt32
FROM 'math_module' :: 'sub'
ABI ROW_DIRECT
DETERMINISTIC
SHA256_HASH 'a957fcb890da31e0e4a72bbdb0e04c50ba7ff5cb6946b21d674f9dcb6e7cd488'
SETTINGS serialization_format = 'MsgPack', webassembly_udf_enable_fuel = 1;
-- ok (218.073ms)
-------------------------
-- 564.642958ms
-- 1 migration
-- 2 sql statements
After the changes are applied, verify with atlas schema inspect:
atlas schema inspect --env local
The output reflects every WASM module and WASM UDF managed by Atlas.