QuickJS
Run JavaScript inside DuckDB SQL via the embedded QuickJS-NG engine. Modern ES2020 syntax, scalar and table-valued forms, parameter passthrough — for the cases where SQL doesn't have the right primitive and Python is too heavy.
Install
-- Install the extension
INSTALL quickjs FROM community;
-- Load it into your session
LOAD quickjs;
-- Evaluate an expression
SELECT quickjs('2 + 2');
-- Call a JS function with arguments — return value is JSON
SELECT quickjs_eval('(a, b) => a + b', 5, 3);
-- Table form: JS returns an array, DuckDB unpacks it into rows
SELECT * FROM quickjs('[1, 2, 3, 4, 5]'); Technical Overview
Why Use QuickJS in DuckDB?
Embed the QuickJS JavaScript engine inside DuckDB so a SELECT can call out to JS for the cases SQL doesn't have a primitive for — modern ECMAScript syntax, scalar and table-valued forms, parameter passthrough. Reach for it when SQL alone can't express the transform; stay in SQL when it can.
🟨 What this extension is for
QuickJS adds a row-by-row JavaScript escape hatch to DuckDB. The shape of the problem is usually one of: text or JSON manipulation that exceeds what regex or json_* functions express cleanly, custom parsing or formatting, or a JS snippet you already have and don't want to port to SQL.
- • Conditional JSON reshaping: When the output shape depends on values inside the input JSON,
quickjs_evalwith a per-row arrow function is more readable — and often faster to write — than a tower ofCASEoverjson_extract. - • Text munging beyond regex: Tokenizers, lightweight parsers, custom slug rules, normalization passes that depend on imperative control flow. JS gives you
for,if,switch, and string methods without leaving SQL. - • Array fan-out: The table form of
quickjslets a JS expression return an array and have DuckDB emit one row per element — handy for unrolling computed sequences or splitting a value into its components. - • Reusing existing JS: Validation rules, formatting helpers, or small library snippets you already trust in JavaScript can be dropped into a query without rewriting them.
⚙️ How it works
The extension links QuickJS-NG — an actively-maintained fork of Fabrice Bellard's QuickJS — into DuckDB as scalar and table functions. Each call instantiates a JS context, evaluates the supplied source, and converts the result back across the SQL boundary. There is no shared state between rows.
- • Two surfaces, picked by call site:
quickjsused afterSELECTevaluates an expression and returns aVARCHAR. The same name used afterFROMexpects an array and unpacks it into rows.quickjs_evalcalls a JS function with positional arguments and returnsJSON. - • Argument binding: Positional arguments to the table form of
quickjsare exposed inside the JS asarg0,arg1, … . When an argument is a JSON-shaped string, a parsed copy is also bound asparsed_arg0,parsed_arg1, … so you don't pay forJSON.parsein the snippet. - • Modern ECMAScript: QuickJS-NG implements the bulk of the ECMAScript standard — arrow functions, destructuring,
let/const,Map/Set,Promise, modernArray/String/Objectmethods. No browser, no Node APIs (nofetch,fs, etc.) — this is a pure language runtime. - • Per-row evaluation: Snippets are JIT-compiled within the QuickJS context per call. There's no cross-row caching of compiled functions, so cost scales linearly with input row count.
🛡️ When to reach for it (and when to stay in SQL)
Row-by-row JavaScript is inherently slower than vectorized SQL. QuickJS is a power feature, not a default — use it when the expressiveness of JS justifies the cost.
- • Prefer native DuckDB functions: If DuckDB's scalar functions,
json_*family, or regex functions cover your transform, they will be substantially faster — they run vectorized over column batches instead of one row at a time. - • Hot OLAP paths are the wrong place: Per-row JS interpretation is fine for ETL and ad-hoc work over millions of rows. It is not the right tool for an interactive dashboard query that runs against a billion-row fact table on every refresh.
- • Untrusted input is risky: QuickJS is a sandbox in the sense that it has no I/O surface. It is not hardened against adversarial JS — don't
quickjs_evaluser-supplied source in a multi-tenant context. - • Consider Rhai for tighter SQL integration:
evalexpr_rhaiembeds the Rhai scripting language. Rhai is more constrained as a language, but it tends to integrate more cleanly with SQL types — fewer JSON round-trips at the boundary. Worth comparing if your snippet is small and type-driven.
🎯 Common Use Cases
Reshape semi-structured payloads
Per-row quickjs_eval with a small arrow function over a JSON column — readable when the output shape branches on values in the input.
Custom string transforms
Slug rules, identifier normalization, lightweight tokenization where regex stops being expressive enough.
Array fan-out from a computed expression
The table form of quickjs — generate a sequence in JS and let DuckDB emit one row per element — useful for unrolling permutations or test data.
Drop in an existing JS helper
Validation logic, date/string formatters, or small library snippets reused inside a SQL pipeline without porting them.
Deep Dive
Technical Details
What you can do with one query
The magic moment — when SQL doesn’t have the right primitive, drop into JavaScript inline:
SELECT id,
quickjs_eval(
'(payload) => {
const d = JSON.parse(payload);
if (d.event === "purchase") {
return { user: d.user, amount: d.amount, ccy: d.meta?.currency ?? "USD" };
}
return { user: d.user, event: d.event };
}',
payload
) AS extracted
FROM events;
quickjs_eval hands each row’s payload to the arrow function, runs it inside the embedded QuickJS-NG engine, and returns the result as JSON — ready to chain into ->> extractors or UNNEST. Conditional shapes that turn into a tower of CASE and json_extract in pure SQL stay short and readable here.
Row-by-row JavaScript is genuinely slower than vectorized SQL. Reach for QuickJS when you actually need expressiveness JavaScript provides — conditional JSON reshaping, text munging beyond regex, custom parsing — not as a first resort.
- If DuckDB’s scalar functions, the
json_*family, or regex functions cover your transform, they will be substantially faster. - This is fine for ETL and ad-hoc analytics over millions of rows. It is not the right tool for an interactive dashboard that runs the same UDF against a billion-row fact table on every refresh.
- Don’t
quickjs_evaluntrusted JavaScript in a multi-tenant context. QuickJS has no I/O surface, but it isn’t hardened against adversarial source.
If your snippet is small and type-driven, the sibling evalexpr_rhai extension embeds Rhai — a more constrained language that tends to integrate more cleanly with SQL types. Worth comparing.
Two surfaces
The extension exposes the same engine two ways. The right one depends on what you want back.
quickjs — used after SELECT, evaluates a JS expression and returns a VARCHAR:
SELECT quickjs('2 + 2'); -- '4'
SELECT quickjs('"hello".toUpperCase()'); -- 'HELLO'
The same name used after FROM is the table form — JS returns an array, DuckDB unpacks it into rows:
SELECT * FROM quickjs('[1, 2, 3].map(x => x * x)');
-- 1, 4, 9
quickjs_eval — calls a JS function with positional arguments and returns the result as JSON. This is the form to use for per-row UDFs:
SELECT quickjs_eval('(a, b) => a + b', 5, 3); -- 8 (JSON)
Argument binding
Positional arguments to the table form of quickjs are exposed inside the snippet as arg0, arg1, … . When an argument is a JSON-shaped string, a parsed copy is also bound as parsed_arg0, parsed_arg1, … so the snippet doesn’t have to call JSON.parse itself:
SELECT *
FROM quickjs('parsed_arg0.map(x => x * arg1)',
'[1, 2, 3, 4, 5]', 3);
-- 3, 6, 9, 12, 15
For quickjs_eval, arguments map to the function’s formal parameters — no argN aliasing.
What’s in the language
The extension links QuickJS-NG, an actively-maintained fork of Fabrice Bellard’s QuickJS. It implements the bulk of the ECMAScript specification — arrow functions, destructuring, let/const, Map/Set, Promise, modern Array / String / Object methods, template literals, optional chaining, nullish coalescing.
There is no browser layer and no Node API surface — no fetch, no fs, no process, no require. This is a pure language runtime, by design. If your snippet needs network or filesystem access, do it in DuckDB’s own httpfs / file readers and pass the resulting value into the JS as an argument.
Performance shape
Each call instantiates a JS context, evaluates the supplied source, and converts the result back across the SQL boundary. There is no cross-row caching of compiled functions, so cost scales linearly with row count. Empirically, expect microseconds-per-row for typical snippets — fine for ETL and ad-hoc work over millions of rows, wrong for hot interactive paths.
The standard playbook:
- Start in pure SQL. If the scalar function reference or
json_*family covers it, you’re done. - If regex isn’t expressive enough or
CASEchains are getting unreadable, reach for QuickJS. - If the snippet is small and type-driven (numbers, strings, simple records), benchmark against
evalexpr_rhai— Rhai’s tighter SQL-type integration sometimes wins.
Reference
Extension Contents
Quick reference to all available functions and settings organized by category.
| Name | Description | |
|---|---|---|
| Execute Run JavaScript code from SQL — | ||
| quickjs() | Evaluate JavaScript code and return the result | |
| quickjs_eval() | Call a JavaScript function with the given arguments | |
API Reference
Function Documentation
Detailed documentation for each function including signatures, parameters, and examples.
quickjs
Signature
Parameters (Positional)
| Parameter | Type | Mode | Description |
|---|---|---|---|
code | VARCHAR | Positional |
Returns
Description
Evaluate JavaScript code and return the result. Has both a scalar form (returns a single value as VARCHAR) and a table-valued form (when the JS returns an array, each element becomes a row).
Examples
Simple expression
SELECT quickjs('2 + 2'); Array → table
SELECT * FROM quickjs('[1, 2, 3, 4, 5]'); Array with parameter passthrough
SELECT * FROM quickjs('parsed_arg0.map(x => x * arg1)', '[1, 2, 3, 4, 5]', 3); Related Functions
quickjs_eval
Signature
Parameters (Positional)
| Parameter | Type | Mode | Description |
|---|---|---|---|
function | VARCHAR | Positional | |
varargs | ANY | Positional | Varargs |
Returns
Description
Call a JavaScript function with the given arguments. The first argument is a JS function expression; subsequent arguments are passed positionally. The result is serialized as JSON.
Examples
Two-argument adder
SELECT quickjs_eval('(a, b) => a + b', 5, 3); Custom JSON munging — better than chaining json_extract_*
SELECT quickjs_eval('(payload) => { const d = JSON.parse(payload); return { user: d.user, os: d.meta.os }; }', payload) AS extracted FROM events; Related Functions
Practical Examples
Cookbook
Real-world recipes and patterns for common use cases.
When to use QuickJS
JavaScript inside SQL isn’t a goal — it’s an escape hatch. Reach for QuickJS when:
- JSON-munging is awkward in SQL alone (deeply nested fields, conditional shapes, computed keys).
- A regex doesn’t quite cut it and you want imperative control flow.
- You have an existing JS library snippet (validation, parsing, formatting) and don’t want to rewrite it.
Don’t reach for it for:
- Anything DuckDB has a native function for. Native primitives are 10–100× faster.
- Long-running computation. QuickJS is fast for an embedded interpreter; it’s still slower than vectorized SQL.
- Untrusted input in a multi-tenant context. Even sandboxed JS engines have a history of escapes.
Scalar form
SELECT quickjs('2 + 2'); -- '4'
SELECT quickjs('let m = "Hello, World!"; m'); -- 'Hello, World!'
SELECT quickjs_eval('(a, b) => a + b', 5, 3); -- 8 (JSON)
For string literals with nested quotes, prefer assigning to a JS variable first to avoid SQL parser confusion.
Table form (array → rows)
SELECT * FROM quickjs('[1, 2, 3, 4, 5]');
-- 5 rows
Pass parameters and have JS return a transformed array:
-- arg0 = raw param, parsed_arg0 = JSON.parse(arg0) when arg0 is a string
SELECT *
FROM quickjs('parsed_arg0.map(x => x * arg1)',
'[1, 2, 3, 4, 5]', 3);
-- → 3, 6, 9, 12, 15
Custom JSON parsing
When you need conditional shapes the standard json_* functions can’t express, QuickJS-as-a-JSON-parser shines:
SELECT id,
event_type,
quickjs_eval(
'(event_type, payload) => {
const d = JSON.parse(payload);
if (event_type === "login") {
return { user: d.user, device: d.meta.device, os: d.meta.os };
} else if (event_type === "purchase") {
return { user: d.user, item: d.meta.item, amount: d.amount };
} else {
return null;
}
}',
event_type, payload
) AS extracted
FROM events;
The result column holds JSON; chain it through json_extract_* or pull fields directly:
SELECT extracted->>'$.user' AS user,
extracted->>'$.os' AS os
FROM transformed_events;
Performance
QuickJS-NG is one of the smallest and fastest embedded JS engines available — but you’re still paying interpretation cost per row. Roughly 1–10 μs per call depending on the snippet. At 100K rows that’s 0.1–1 second, which is fine for most ETL but bad for hot OLAP paths.
Where possible, prefer:
- Native DuckDB scalar functions.
- The
json_*family for JSON. - QuickJS only when 1 and 2 don’t fit.
Platform Support
Compatibility
Extension availability may vary by platform and DuckDB version. Check below to ensure this extension supports your environment before installation.
Quick Facts
Platforms
Supported platform architectures
Compiled binary sizes
| Platform | Architecture | Size |
|---|---|---|
| Linux | aarch64 | 3.10 MB |
| Linux | x86_64 | 3.50 MB |
| Linux (musl) | x86_64 | 2.77 MB |
| macOS | Apple Silicon | 2.21 MB |
| macOS | Intel | 2.55 MB |
| Windows | x86_64 | 7.91 MB |
| WASM | eh | 19.7 KB |
| WASM | mvp | 16.2 KB |
| WASM | threads | 16.1 KB |
Gzipped download size from the DuckDB community-extensions registry.
JavaScript in Your SELECT
Install QuickJS to run snippets and functions written in JavaScript inside DuckDB queries — JSON munging, custom transforms, anything ES2020 can express.