WASM Triggers & Polyglot Stored Procedures
UVP
Write database triggers and stored procedures in Rust, JavaScript, Python, or any language that compiles to WebAssembly — sandboxed, capability-gated, with <1 ms call overhead, three-tier caching (instance pool / module / persistent AOT), and 50+ sandbox-escape tests with zero failures. SQL CREATE FUNCTION … LANGUAGE javascript|python|rust|wasm syntax. No external service, no Docker sidecar — your trigger runs in the same process as your query, behind a Wasmtime sandbox you can’t escape.
Prerequisites
- HeliosDB Full v8.0.3
- ~25 minutes
- Familiarity with at least one of Rust, JavaScript, or Python
- (Optional)
wasm32-unknown-unknownRust target if you want native WASM —rustup target add wasm32-unknown-unknown
The WASM subsystem is always compiled in Full. The runtimes (Wasmtime for native WASM, QuickJS for JS, RustPython for Python) are statically linked.
1. Two Things to Distinguish
| Concept | Crate | What it does |
|---|---|---|
| Stored procedures | heliosdb-wasm/crates/procedures | Functions you call explicitly: SELECT my_func(...) |
| Triggers | heliosdb-wasm/crates/triggers | Functions that fire on INSERT/UPDATE/DELETE automatically |
Both use the same polyglot runtime (heliosdb-wasm/crates/runtime). A trigger is “just” a stored procedure that the engine calls on a table event.
2. Your First Stored Procedure — JavaScript
CREATE FUNCTION calculate_discount(price REAL, discount_rate REAL)RETURNS REALLANGUAGE javascriptAS $$ function calculate_discount(price, discount_rate) { return price * (1 - discount_rate); }$$;
SELECT calculate_discount(100.0, 0.15); -- 85That’s the entire flow. The function is parsed, compiled by QuickJS, cached in the L1 instance pool, and ready to call again in <0.5 ms.
3. Same Thing, Four Languages
-- PythonCREATE FUNCTION process_data(items JSON)RETURNS JSONLANGUAGE pythonAS $$ def process_data(items): return [ {'id': item['id'], 'value': item['value'] * 2} for item in items if item['value'] > 0 ]$$;
-- Rust (compiled to WASM, registered as binary)CREATE FUNCTION fibonacci(n INTEGER)RETURNS INTEGERLANGUAGE rustAS $$ -- (binary WASM module bytes) $$;
-- Raw WebAssembly (any language that targets WASM)CREATE FUNCTION custom_compute(input BYTEA)RETURNS BYTEALANGUAGE wasmAS $$ -- (binary .wasm) $$;Default memory budgets
| Runtime | Default memory | Stable APIs |
|---|---|---|
| QuickJS (JS) | 16 MB | ES6+, async/await |
| RustPython (Python) | 32 MB | Standard library |
| Native WASM (Rust) | configurable | full Wasmtime API |
You can override per function:
ALTER FUNCTION expensive_functionSET max_memory_bytes = 67108864, -- 64 MB max_cpu_time_ms = 30000, -- 30 s max_instructions = 50000000000; -- 50 billion4. Triggers — BEFORE / AFTER, ROW / STATEMENT
The trigger system supports all the combinations:
| Timing | Granularity | Events |
|---|---|---|
| BEFORE | ROW | INSERT / UPDATE / DELETE |
| BEFORE | STATEMENT | INSERT / UPDATE / DELETE |
| AFTER | ROW | INSERT / UPDATE / DELETE |
| AFTER | STATEMENT | INSERT / UPDATE / DELETE |
A trigger can fire on multiple events:
CREATE TRIGGER audit_changesAFTER INSERT OR UPDATE OR DELETE ON ordersFOR EACH ROWEXECUTE FUNCTION audit_log_row();Trigger function in JavaScript
CREATE FUNCTION audit_log_row()RETURNS TRIGGERLANGUAGE javascriptAS $$ function audit_log_row(ctx) { // ctx.OLD and ctx.NEW are populated by the engine const event = { table: ctx.TG_TABLE_NAME, op: ctx.TG_OP, // 'INSERT' | 'UPDATE' | 'DELETE' old: ctx.OLD, new: ctx.NEW, user: ctx.session.user, time: new Date().toISOString(), }; // capability check is enforced — DATABASE_WRITE('audit_log') required ctx.db.insert('audit_log', event); return ctx.NEW; // BEFORE row triggers can mutate NEW }$$;Trigger function in Python
CREATE FUNCTION enforce_amount_positive()RETURNS TRIGGERLANGUAGE pythonAS $$ def enforce_amount_positive(ctx): if ctx.NEW['amount'] < 0: raise ValueError(f"amount must be non-negative; got {ctx.NEW['amount']}") return ctx.NEW$$;
CREATE TRIGGER check_amountBEFORE INSERT OR UPDATE ON ordersFOR EACH ROWEXECUTE FUNCTION enforce_amount_positive();A BEFORE … FOR EACH ROW trigger that raises rolls back the operation.
5. Capability-Based Security
The runtime is capability-deny-by-default. A function can do nothing dangerous unless you explicitly grant it.
-- Allow a specific table readALTER FUNCTION my_functionGRANT CAPABILITY DATABASE_READ('users');
-- Allow a specific external host (egress)ALTER FUNCTION my_functionGRANT CAPABILITY NETWORK_ACCESS('api.example.com');
-- Allow a write to a specific tableALTER FUNCTION audit_log_rowGRANT CAPABILITY DATABASE_WRITE('audit_log');
-- RevokeALTER FUNCTION my_functionREVOKE CAPABILITY FILE_WRITE('*');Pre-built policies
ALTER FUNCTION strict_function SET SECURITY POLICY MINIMAL; -- nothingALTER FUNCTION normal_function SET SECURITY POLICY DEFAULT; -- basic capabilitiesALTER FUNCTION trusted_function SET SECURITY POLICY PERMISSIVE; -- extendedWhat you cannot do (sandbox)
- Direct system calls (filtered through a whitelist)
- File access outside granted paths
- Network egress outside whitelisted hosts
- Fork / exec
- Direct memory peek into the host
50+ sandbox-escape tests run on every change. Zero have ever passed.
6. Performance — The Three-Tier Cache
The runtime caches compiled modules at three levels:
| Tier | What’s cached | Hit latency |
|---|---|---|
| L1 | Hot WASM instances (already-instantiated) | <1 ms |
| L2 | Compiled WASM modules | ~1 ms |
| L3 | Persistent AOT cache (disk) | ~10 ms |
Benchmarks (from the crate’s own bench suite)
| Operation | p50 | Throughput |
|---|---|---|
| Simple JS function | <0.5 ms | >20,000 req/s |
| Python calculation | <1 ms | >10,000 req/s |
| Native WASM | <0.2 ms | >50,000 req/s |
| Cached result | <0.05 ms | >100,000 req/s |
| Parallel batch (10) | ~1 ms | >10,000 batches/s |
JIT levels
| Level | Compile time | Runtime speedup |
|---|---|---|
| Low Latency | ~1 ms | 1.0× |
| Balanced | ~5 ms | 1.5× |
| Fast | ~20 ms | 2-3× |
Pick Low Latency for short-lived functions, Fast for long-running batch jobs.
7. Real-World Patterns
a. Audit log trigger (you’ll write this on day one)
CREATE FUNCTION audit_row()RETURNS TRIGGERLANGUAGE javascriptAS $$ function audit_row(ctx) { ctx.db.insert('audit_log', { table: ctx.TG_TABLE_NAME, op: ctx.TG_OP, old: ctx.OLD, new: ctx.NEW, user: ctx.session.user, ts: new Date().toISOString(), }); return ctx.NEW; }$$;
ALTER FUNCTION audit_row GRANT CAPABILITY DATABASE_WRITE('audit_log');
CREATE TRIGGER audit_usersAFTER INSERT OR UPDATE OR DELETE ON usersFOR EACH ROW EXECUTE FUNCTION audit_row();b. Constraint trigger — derived field
CREATE FUNCTION compute_full_name()RETURNS TRIGGERLANGUAGE javascriptAS $$ function compute_full_name(ctx) { ctx.NEW.full_name = `${ctx.NEW.first_name} ${ctx.NEW.last_name}`; return ctx.NEW; }$$;
CREATE TRIGGER set_full_nameBEFORE INSERT OR UPDATE OF first_name, last_name ON peopleFOR EACH ROW EXECUTE FUNCTION compute_full_name();c. Webhook on INSERT (with capability)
CREATE FUNCTION notify_external()RETURNS TRIGGERLANGUAGE javascriptAS $$ async function notify_external(ctx) { await ctx.fetch('https://hooks.example.com/notify', { method: 'POST', body: JSON.stringify(ctx.NEW), }); return ctx.NEW; }$$;
ALTER FUNCTION notify_externalGRANT CAPABILITY NETWORK_ACCESS('hooks.example.com');
CREATE TRIGGER notify_on_orderAFTER INSERT ON ordersFOR EACH ROW EXECUTE FUNCTION notify_external();Note: AFTER triggers should not block the transaction on slow network calls — for fire-and-forget at scale, use the CDC streaming pipeline instead.
8. Hot Reload (Development Only)
ALTER FUNCTION my_function SET hot_reload = true;The runtime detects source changes and rebuilds the module. Don’t enable in production — module rebuilds invalidate the L1/L2 cache and add latency.
9. Type Conversions
Each runtime maps SQL types to native types automatically:
| SQL | JavaScript | Python | Rust |
|---|---|---|---|
| INTEGER | number | int | i64 |
| REAL | number | float | f64 |
| TEXT | string | str | String |
| BYTEA | Uint8Array | bytes | Vec<u8> |
| JSON | object | dict/list | serde_json::Value |
| NUMERIC | string (precision-preserving) | Decimal | BigDecimal |
NULL always becomes the language’s null/None equivalent.
10. Production Checklist
- Every function has the minimum capability set — no blanket
PERMISSIVE - Network-egress functions whitelist explicit hosts (no
'*') - Memory and CPU limits are set on heavy functions
- BEFORE-row triggers don’t make slow network calls (will block transactions)
- AFTER triggers that need to call external services use CDC instead, or fail-soft
- Hot reload is off in prod
- Audit logging is enabled (
max_audit_events = 10_000is the default; size for your workload) - You’ve reviewed the JIT level —
Fastfor batch,Low Latencyfor OLTP
Verification Status
| Subsystem | Status |
|---|---|
| JavaScript / QuickJS | Production · 100% complete |
| Python / RustPython | Production · 100% complete |
| Native WASM (Wasmtime) | Production · 100% complete |
| Triggers (BEFORE/AFTER, ROW/STATEMENT) | Production (crate is Active — multi-event triggers documented in the crate README) |
| Capability security | Production · 50+ sandbox-escape tests, 0 failures |
| Three-tier caching | Production |
| Hot reload | Development feature; do not enable in prod |
| Go support | Roadmap — listed under “future enhancements” |
Where Next
- CDC Streaming — for fire-and-forget side effects, prefer CDC over heavy AFTER triggers
- Edge Deployment — same WASM functions run on edge nodes
- Multi-Tenancy — per-tenant function quotas
References
- Source:
/home/app/Helios/Full/heliosdb-wasm/crates/{triggers,procedures,runtime,sdk*}/ - Function development guide:
heliosdb-wasm/docs/FUNCTION_DEVELOPMENT_GUIDE.md - Security model:
heliosdb-wasm/docs/SECURITY_MODEL.md - Performance tuning:
heliosdb-wasm/docs/PERFORMANCE_TUNING.md