🟨

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_eval with a per-row arrow function is more readable — and often faster to write — than a tower of CASE over json_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 quickjs lets 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: quickjs used after SELECT evaluates an expression and returns a VARCHAR. The same name used after FROM expects an array and unpacks it into rows. quickjs_eval calls a JS function with positional arguments and returns JSON.
  • Argument binding: Positional arguments to the table form of quickjs are exposed inside the JS as arg0, arg1, … . When an argument is a JSON-shaped string, a parsed copy is also bound as parsed_arg0, parsed_arg1, … so you don't pay for JSON.parse in the snippet.
  • Modern ECMAScript: QuickJS-NG implements the bulk of the ECMAScript standard — arrow functions, destructuring, let/const, Map/Set, Promise, modern Array / String / Object methods. No browser, no Node APIs (no fetch, 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_eval user-supplied source in a multi-tenant context.
  • Consider Rhai for tighter SQL integration: evalexpr_rhai embeds 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.

A power feature, not a default

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_eval untrusted 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:

  1. Start in pure SQL. If the scalar function reference or json_* family covers it, you’re done.
  2. If regex isn’t expressive enough or CASE chains are getting unreadable, reach for QuickJS.
  3. 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(...) for general expressions and array → table fan-out, quickjs_eval(fn, ...args) for invoking a JS function with positional arguments and getting JSON back.

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

Scalar Function Execute
Signature
quickjs(code: VARCHAR) → VARCHAR
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
1

Simple expression

SELECT quickjs('2 + 2');
2

Array → table

SELECT * FROM quickjs('[1, 2, 3, 4, 5]');
3

Array with parameter passthrough

SELECT * FROM quickjs('parsed_arg0.map(x => x * arg1)', '[1, 2, 3, 4, 5]', 3);

quickjs_eval

Scalar Function Execute
Signature
quickjs_eval(function: VARCHAR, varargs: ANY) → JSON
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
1

Two-argument adder

SELECT quickjs_eval('(a, b) => a + b', 5, 3);
2

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;

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:

  1. Native DuckDB scalar functions.
  2. The json_* family for JSON.
  3. 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

Software License MIT
Pricing Free
Written In C++
Source Available Yes
View on GitHub
Usage
12,600,981+
loads in last 30 days

Platforms

Linux
Linux (musl)
macOS
Windows
WASM
Supported platform architectures
Linux: x86_64, aarch64
Linux (musl): x86_64
macOS: Intel, Apple Silicon
Windows: x86_64
WASM: threads, eh, mvp
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.

DuckDB Versions

Release calendar
Supported
v1.4.4 v1.5.2

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.