Skip to content

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-unknown Rust 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

ConceptCrateWhat it does
Stored proceduresheliosdb-wasm/crates/proceduresFunctions you call explicitly: SELECT my_func(...)
Triggersheliosdb-wasm/crates/triggersFunctions 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 REAL
LANGUAGE javascript
AS $$
function calculate_discount(price, discount_rate) {
return price * (1 - discount_rate);
}
$$;
SELECT calculate_discount(100.0, 0.15); -- 85

That’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

-- Python
CREATE FUNCTION process_data(items JSON)
RETURNS JSON
LANGUAGE python
AS $$
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 INTEGER
LANGUAGE rust
AS $$ -- (binary WASM module bytes) $$;
-- Raw WebAssembly (any language that targets WASM)
CREATE FUNCTION custom_compute(input BYTEA)
RETURNS BYTEA
LANGUAGE wasm
AS $$ -- (binary .wasm) $$;

Default memory budgets

RuntimeDefault memoryStable APIs
QuickJS (JS)16 MBES6+, async/await
RustPython (Python)32 MBStandard library
Native WASM (Rust)configurablefull Wasmtime API

You can override per function:

ALTER FUNCTION expensive_function
SET max_memory_bytes = 67108864, -- 64 MB
max_cpu_time_ms = 30000, -- 30 s
max_instructions = 50000000000; -- 50 billion

4. Triggers — BEFORE / AFTER, ROW / STATEMENT

The trigger system supports all the combinations:

TimingGranularityEvents
BEFOREROWINSERT / UPDATE / DELETE
BEFORESTATEMENTINSERT / UPDATE / DELETE
AFTERROWINSERT / UPDATE / DELETE
AFTERSTATEMENTINSERT / UPDATE / DELETE

A trigger can fire on multiple events:

CREATE TRIGGER audit_changes
AFTER INSERT OR UPDATE OR DELETE ON orders
FOR EACH ROW
EXECUTE FUNCTION audit_log_row();

Trigger function in JavaScript

CREATE FUNCTION audit_log_row()
RETURNS TRIGGER
LANGUAGE javascript
AS $$
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 TRIGGER
LANGUAGE python
AS $$
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_amount
BEFORE INSERT OR UPDATE ON orders
FOR EACH ROW
EXECUTE 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 read
ALTER FUNCTION my_function
GRANT CAPABILITY DATABASE_READ('users');
-- Allow a specific external host (egress)
ALTER FUNCTION my_function
GRANT CAPABILITY NETWORK_ACCESS('api.example.com');
-- Allow a write to a specific table
ALTER FUNCTION audit_log_row
GRANT CAPABILITY DATABASE_WRITE('audit_log');
-- Revoke
ALTER FUNCTION my_function
REVOKE CAPABILITY FILE_WRITE('*');

Pre-built policies

ALTER FUNCTION strict_function SET SECURITY POLICY MINIMAL; -- nothing
ALTER FUNCTION normal_function SET SECURITY POLICY DEFAULT; -- basic capabilities
ALTER FUNCTION trusted_function SET SECURITY POLICY PERMISSIVE; -- extended

What 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:

TierWhat’s cachedHit latency
L1Hot WASM instances (already-instantiated)<1 ms
L2Compiled WASM modules~1 ms
L3Persistent AOT cache (disk)~10 ms

Benchmarks (from the crate’s own bench suite)

Operationp50Throughput
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

LevelCompile timeRuntime speedup
Low Latency~1 ms1.0×
Balanced~5 ms1.5×
Fast~20 ms2-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 TRIGGER
LANGUAGE javascript
AS $$
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_users
AFTER INSERT OR UPDATE OR DELETE ON users
FOR EACH ROW EXECUTE FUNCTION audit_row();

b. Constraint trigger — derived field

CREATE FUNCTION compute_full_name()
RETURNS TRIGGER
LANGUAGE javascript
AS $$
function compute_full_name(ctx) {
ctx.NEW.full_name = `${ctx.NEW.first_name} ${ctx.NEW.last_name}`;
return ctx.NEW;
}
$$;
CREATE TRIGGER set_full_name
BEFORE INSERT OR UPDATE OF first_name, last_name ON people
FOR EACH ROW EXECUTE FUNCTION compute_full_name();

c. Webhook on INSERT (with capability)

CREATE FUNCTION notify_external()
RETURNS TRIGGER
LANGUAGE javascript
AS $$
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_external
GRANT CAPABILITY NETWORK_ACCESS('hooks.example.com');
CREATE TRIGGER notify_on_order
AFTER INSERT ON orders
FOR 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:

SQLJavaScriptPythonRust
INTEGERnumberinti64
REALnumberfloatf64
TEXTstringstrString
BYTEAUint8ArraybytesVec<u8>
JSONobjectdict/listserde_json::Value
NUMERICstring (precision-preserving)DecimalBigDecimal

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_000 is the default; size for your workload)
  • You’ve reviewed the JIT level — Fast for batch, Low Latency for OLTP

Verification Status

SubsystemStatus
JavaScript / QuickJSProduction · 100% complete
Python / RustPythonProduction · 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 securityProduction · 50+ sandbox-escape tests, 0 failures
Three-tier cachingProduction
Hot reloadDevelopment feature; do not enable in prod
Go supportRoadmap — listed under “future enhancements”

Where Next


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