Compiling Isn't Running: Functionally Testing DuckDB-WASM Extensions
A DuckDB extension that compiles for WebAssembly has only proven that it compiles. Whether it loads, and whether it actually runs, are separate questions. I built a Node harness to ask them across 124 community extensions. Here's what it found and the fixes that came out of it.
| Outcome | Extensions | Share |
|---|---|---|
| pass | ||
| fail | ||
| skip | ||
| not-deployed | ||
| no-tests | ||
| crash |
Survey of 124 DuckDB community extensions. Shares are of the 124 surveyed and are rounded.
When the WebAssembly build of a DuckDB extension passes CI, itâs tempting to read that as âthe extension works.â It doesnât mean that. It means the code compiled: emcc accepted the source, produced a .duckdb_extension.wasm, and the file got uploaded to the catalog. Whether that file loads into a running engine, and whether it does anything useful once loaded, are questions the build never asks.
For the WASM extensions in Haybarn, Query.Farmâs DuckDB distribution, nobody was asking them either. So I went looking, and the first extension I checked had been broken for weeks while its badge stayed green.
So I built a harness to run all of them. Hereâs the short version of this whole post â every WASM-enabled community extension, run against the published engine and graded on its own test suite:
That gap matters for two kinds of people. If you publish a DuckDB extension, the WASM build is the one target you probably canât test by hand, and a green badge can hide a binary that throws the instant a browser user calls a function. If you build on DuckDB-WASM, an extension you rely on may be quietly missing in the browser while working everywhere else. Either way, the failure surfaces at your users instead of your CI, which is the worst place to find it.
The canary
The jsonata extension lets you run JSONata expressions over JSON inside SQL. Its WASM build compiled fine, shipped to the catalog, and installed without complaint:
INSTALL jsonata FROM community; -- okLOAD jsonata; -- okSELECT jsonata('Account', '{"Account": 5}');-- TypeError: n is not a functionINSTALL and LOAD both succeed. The first real function call throws an opaque TypeError from inside the worker. A test that stops at âdoes it loadâ passes this and ships it. But nobody installs an extension just to load it; they call its functions, which is where this one fell over.
Native builds catch exactly this. make test links the extension into DuckDBâs unittest binary and runs the extensionâs own test/sql/*.test sqllogictest files against it. The WASM build runs none of that, because thereâs no WASM unittest binary; the test runner is native C++. So WASM has no functional test layer at all.
Why âit compiledâ tells you so little in WASM
On native, a loadable DuckDB extension is a shared library the engine dlopens. In WASM itâs an emscripten side module, linked with -sSIDE_MODULE=2, that the engineâs dynamic linker loads at runtime and resolves against the main module. Anything it canât resolve doesnât fail the load; it becomes a stub that only blows up when something calls it.
That gap, between compiling and resolving, is where a clean compile can still hide a broken extension. It breaks four ways, and the census hit all of them: a dependency that never got linked into the .wasm, a file read that assumes a real filesystem, an HTTP call that assumes real sockets, or a hard dependency on another extension that has no WASM build. None of this shows up at compile time; you only see it when you run the extension.
The plan: run their own tests, against the real engine, in Node
I didnât want to write new tests. Every extension already ships a test/sql/*.test suite, the same sqllogictest files the native build runs. Those assertions are a much better oracle than any smoke query Iâd invent, so the harness runs them against the published WASM engine, the way a user would actually get the extension.
The harness, haybarn-extension-wasm-tester (now open source), does this per extension:
- Census. Parse the community catalog descriptors, select the ones with WASM enabled.
- Fetch. Shallow-clone each extensionâs repo at the exact ref the catalog shipped, and find its
test/sql/*.testfiles. - Run. Spin up a fresh
@haybarn/haybarn-wasmengine in Node, install and load the extension from the catalog the way a user would, and run the test fileâs records against it. - Compare. Match results and errors the way sqllogictest does.
Why Node instead of a browser? Node has a real filesystem, so the engine installs and caches extensions exactly the way the published packaging does, and it runs the same wasm_eh engine binary and the same extension binary a browser would pull.
What it found
The failures fell into five groups. Each has a specific cause, and most of the fixes are a few lines. One thing to be clear about first: these are bugs in the extensions themselves â in their source, or their build configuration â not in the WASM engine. They reproduce on the official duckdb-wasm build just the same. The harness is only the thing that finally ran them.
1. The missing library (LINKED_LIBS)
This is the n is not a function class, and it was the most common. The extension depends on a vcpkg C++ library â yaml-cpp, LibXml2, libxxhash, a QuickJS runtime â and links it the normal way, with target_link_libraries:
target_link_libraries(${LOADABLE_EXTENSION_NAME} yaml-cpp::yaml-cpp)Correct for native. Ignored by the -sSIDE_MODULE=2 link, which only honors libraries named in the extension descriptorâs LINKED_LIBS. The dependencyâs symbols are left undefined in the .wasm. The module still loads, and then the first call into the missing code throws. The fix is to name the library where the WASM link will actually see it:
duckdb_extension_load(yaml LINKED_LIBS "$<TARGET_FILE:yaml-cpp::yaml-cpp>")That one move fixed hashfuncs, marisa, textplot, json_schema, quickjs, and jsonata itself.
2. Raw file I/O instead of DuckDBâs filesystem
An extension that reads its input with fopen or std::fstream works everywhere except the one place that has no host filesystem. A Stata-file reader, a FIT-file reader, a couple of others all tripped on this. The fix is to read through DuckDBâs own filesystem abstraction:
auto &fs = FileSystem::GetFileSystem(context);auto handle = fs.OpenFile(path, FileFlags::FILE_FLAGS_READ);// read via the handle instead of a FILE*The bonus: for any path it opens through that abstraction, the extension also gets s3://, https://, and registered-buffer support on native builds. Raw fopen was leaving that on the table.
3. Raw HTTP instead of HTTPUtil
An extension that opens its own httplib client needs OS sockets, which WASM doesnât have. DuckDB ships an HTTP abstraction (HTTPUtil) thatâs already wired to the browserâs HTTP stack under WASM and to the normal stack natively. Routing requests through it makes the extension work in WASM and inherit DuckDBâs proxy and TLS settings on native. (Occasionally you find the opposite: one âHTTP statsâ extension already subclassed HTTPUtil correctly, and its only failure was a test that hit a live network endpoint.)
4. A dependency that doesnât exist in WASM
The delta_classic extension delegates all its work to the core delta extension, and delta is a Rust extension (delta-kernel-rs) that no DuckDB distribution builds for WASM, upstream included. The native builds are on the catalog, but extensions.duckdb.org/<version>/wasm_eh/delta.duckdb_extension.wasm 404s for every version I checked. No amount of fixing delta_classic helps; its dependency canât be there. The honest fix is to mark it WASM-excluded so users get a clear message instead of a baffling signature error.
5. Tests that were never WASMâs fault
A few âfailuresâ were the test files, not the engine â a suite asserting True/False where the engine renders true/false, a numeric test rounding to one more decimal place than the platform reproduces. Real signal, just pointed at the test rather than the binary. Fixing those (and teaching the comparator that true and 1 are the same boolean) cleared them.
The scoreboard
Hereâs one full census run: Haybarn engine v1.5.3, wasm_eh, every WASM-enabled community extension, tested on June 13, 2026.
| Status | Count | Meaning |
|---|---|---|
pass |
58 | every runnable record passed |
fail |
43 | a record produced a wrong result or unexpected error |
skip |
9 | every test file used directives the runner doesnât support |
no-tests |
5 | the repo ships no test/sql/*.test files |
not-deployed |
8 | declared WASM-enabled, but no artifact on the catalog |
crash |
1 | the engine died loading it |
| Total surveyed | 124 |
That run executed 11,219 sqllogictest records across 1,088 test files, with 873 record failures. If you drop the categories that arenât really a verdict on a shipped binary (not-deployed, no-tests, skip), 102 extensions actually ran records (pass, fail, or crash), and 58 of them pass â a 57% pass rate. A failure here usually doesnât mean a broken extension so much as one that hasnât been adapted to WASMâs constraints yet: no filesystem, no sockets, stricter linking.
The fixes arenât hypothetical. The 18 WASM extensions Query.Farm maintains all pass now â several of them used to load and then throw on the first call â and the harness will catch it if any regress. The same fixes apply to everyone elseâs extensions, which is why I sent them upstream.
I didnât root-cause all 43 failures, but for the third-party ones I could diagnose, I opened fifteen issues (all linked in the appendix below). Each names the file and symbol, explains the LINKED_LIBS, filesystem, or HTTP fix, and includes code to copy. Theyâre written against DuckDB-WASM in general rather than Haybarn specifically, since the patterns apply to any distribution.
What Iâd tell anyone shipping WASM extensions
A passing compile only tells you the linker was happy. For WASM the gap between that and a working extension is wide, because most of the failures live in runtime symbol resolution and the absence of a filesystem or sockets. The only way to know is to load the artifact and call into it.
When you do, the fixes are usually small: a LINKED_LIBS entry, a FileSystem::OpenFile instead of fopen, an HTTPUtil call instead of raw sockets. The hard part was never the fix. Itâs seeing the failure at all, which a green compile badge will happily hide.
Thereâs nothing Haybarn-specific about the check itself â it runs any extensionâs own test suite against any duckdb-wasm engine, so it would fit just as well in the community-extensions release pipeline upstream. Once itâs had proper review, Iâd like to see DuckDB run something like it before publishing WASM artifacts. A compile that never executed isnât much of a guarantee, and this is a cheap way to turn it into one.
The harness is on GitHub, MIT-licensed. Itâs standalone on purpose, not wired into any build pipeline, so I can point it at the catalog by hand whenever I want a real answer instead of a compile:
# one extension, or drop --only to run the whole catalognode bin/test.mjs --community-dir ../haybarn-community-extensions --only jsonataAppendix: full results
Engine v1.5.3, wasm_eh, tested June 13, 2026. This is a snapshot; some of the failing extensions below have fixes filed or already deployed since.
Each extension links to its source repository.
Pass (58)
a5astrobitfiltersboilstreambvh2sqlcapi_quackcelestialchaoscloudfrontdatasketchesdecimal_arithmeticdqtestduck_block_utilsduck_delta_shareduck_geoarrowduckdb_delta_sharingduckhtsducksynceenddbevalexpr_rhaifivetranfunc_applygeosiloghh3hashfuncsinflectorjson_schemajsonatalastralindelmarisaminijinjaminiplotnanoarrowoastopen_promptovertureparser_toolspbixpfcpoachedpolyglotpsyduckquickjsrapidfuzzread_dbfread_linesse3snowflakestochasticteratextplottsidvgiwaddlewireduckzeek
Fail (43)
These didnât pass on the test date. In most cases thatâs a small, WASM-specific gap rather than a broken extension; where I diagnosed one, the specific fix is in the issues filed below.
anndataanofox_forecastanofox_statisticsclampcronjobdashdelta_classicdplyrdtaduck_dggsduck_huntduckdb_mcpduckdb_zarrducksmileseeagrideurostatfire_duck_extfithnsw_acornhostfshttp_statsionlptsluamagicmarkdownosmiumotlppacprqlpsqlpstquack_oauthrdfread_statscalarfsscroogesistatspxlsxweb_archivewebbedyamlzipfs
Crash (1): netquack
Skip â unsupported test directives, no functional verdict (9): arrow, cozip, delta_export, harbor, mpduck, nsv, pivot_table, sitemap, web_search
No tests in repo (5): duckdbi, duckgl, ducklake_cdc, duckorch, sheetreader
Declared WASM-enabled but not deployed to the catalog (8): elasticsearch, flock, gdx, miint, nats_js, sitting_duck, valhalla_routing, webmacro
Issues filed (15)
The issues I opened on third-party extensions, grouped by the fix each one recommends.
Read files through DuckDBâs FileSystem instead of raw fopen/fstream:
dtaâ codedthinking/duckdb-dta#2fitâ antoriche/duckdb-fit-extension#1read_statâ dylanmeysmans/duckdb-read-stat#11osmiumâ jake-low/duckdb-osmium#4
Name the dependency in LINKED_LIBS so the WASM link includes it:
yamlâ teaguesterling/duckdb_yaml#40webbedâ teaguesterling/duckdb_webbed#96rdfâ nonodename/duck_rdf#39luaâ isaacbrodsky/duckdb-lua#25
Make HTTP requests through DuckDBâs HTTPUtil (or exclude WASM) instead of raw sockets:
sistatâ fklezin/duckdb-sistat#7spxlsxâ paulmupeters/spxlsx#2eurostatâ ahuarte47/duckdb-eurostat#4
Exclude the WASM platforms, because the extension canât work there as built:
fire_duck_extâ BorisBesky/fire_duck_ext#5 â Firestore over raw HTTPquack_oauthâ DataZooDE/quack-oauth#3 â OAuth redirect flowsanndataâ honicky/anndata-duckdb-extension#24 â HDF5 file accessdelta_classicâ djouallah/delta_classic#1 â depends ondelta, which has no WASM build