This expression ought to return 2:

SELECT quickjs_eval('(x) => x + 1', 1::SMALLINT) AS incremented;
result
incremented
2

In the old build, the argument arrived in JavaScript as a string. JavaScript did exactly what it does with '1' + 1: it returned '11'. The same problem affected TINYINT and decimals. Casting a column could change the meaning of the JavaScript expression without changing the expression itself.

The DuckDB 2.0 update to QuickJS fixes that conversion and expands it to the nested and temporal types you are likely to pass from a query. We also added controls for scripts that allocate too much memory or never finish, and updated the embedded engine to QuickJS-NG 0.18.0.

Use the value you already have

There is no need to turn a list into JSON text just to use map:

SELECT quickjs_eval('(values) => values.map(x => x * x)', [1, 2, 3]) AS squares;
result
squares
[1,4,9]

A row or struct becomes an object, including its nested values:

SELECT quickjs_eval(
'(item) => item.name + ":" + item.tags.join("|")',
{'name': 'duck', 'tags': ['a', 'b']}
) AS label;
result
label
"duck:a|b"

quickjs_eval returns JSON, which is why the string result includes quotes. The scalar quickjs(code) function returns text, while the table form turns an array result into rows.

The conversions are defined by the input type:

DuckDB value JavaScript value
Booleans, integers within the safe range, floats, decimals Boolean or Number
Integers outside the safe Number range BigInt
LIST, ARRAY Array
STRUCT, MAP Object
UNION Its active member
Finite dates and timestamps Date, in UTC at millisecond precision
BLOB Uint8Array

Other types, including UUIDs and intervals, use their string representation. Decimals become JavaScript Numbers, so this is not an exact decimal arithmetic interface. Timestamps finer than milliseconds lose that extra precision when converted to Date; infinite dates remain strings.

Keep the digits in large integers

JavaScript Numbers cannot represent every integer above 253 − 1. Passing a 64-bit identifier through that conversion can quietly change the identifier.

The extension now uses BigInt outside the safe Number range:

SELECT quickjs_eval('(x) => typeof x', 9007199254740993::BIGINT) AS type,
quickjs_eval('(x) => x', 9007199254740993::BIGINT) AS value;
result
typevalue
"bigint"9007199254740993

BigInt results are serialized as JSON numbers with their digits intact. A later consumer can still lose precision if it parses that JSON into ordinary JavaScript Numbers. The boundary here preserves the value; downstream code must do the same.

This also affects arithmetic in existing scripts. JavaScript will not mix a BigInt with a Number in x + 1. Use a BigInt literal, such as 1n, when the input is known to be a BigInt, or handle the two cases explicitly when a column crosses the safe-integer boundary.

Stop a script without stopping DuckDB

A QuickJS runtime now has a default heap limit of 256MB. It is a limit on each runtime, with one runtime per execution thread and query, rather than a total budget for all JavaScript running in the database.

You can lower that limit and set a timeout:

SET quickjs_memory_limit = '64MB';
SET quickjs_timeout_ms = 200;
SELECT quickjs_eval('(n) => n * 2', 21) AS doubled;
result
doubled
42

The timeout applies to each scalar evaluation, or to the whole call of the table function. Its default is 0, meaning disabled. The interrupt handler also checks DuckDB’s query deadline and cancellation, including while processing asynchronous promise jobs.

An allocation failure must remain a failure even if the script catches JavaScript exceptions. The extension tracks whether the runtime hit its memory limit and reports it after evaluation. It also handles the awkward case where QuickJS has too little memory left to allocate an error object. That now reports an out-of-memory error instead of an unexplained null exception.

A few smaller corrections matter

The table function now accepts multi-statement scripts and uses their completion value:

SELECT value FROM quickjs('let a = [1, 2]; a.push(arg0); a', 3) AS results(value);
result · 3 rows
value
1
2
3

Plain string arguments no longer have to survive an unconditional JSON.parse. Embedded NUL bytes retain their length. quickjs_eval returns SQL NULL for undefined, while JavaScript null remains JSON null.

Recursion and deeply nested inputs have stack checks. Returning or throwing a Symbol no longer follows a failed string conversion into a process crash. These cases are recorded in the regression tests, alongside the type conversions and resource limits.

The embedded environment still has no filesystem, network, process, or module access. It is useful for applying JavaScript logic to query data. The extension page documents all three forms; when upgrading, start with scripts that parse arguments themselves or depend on their old string types.