在 DuckDB 里跑 JavaScript:duckdb-luajit 内嵌 QuickJS
duckdb-luajit 是 DuckDB 的 LuaJIT UDF 扩展:SQL 里写 Lua 函数,ffi直接调 C 库。
这篇文章实测它新增的一条路: SQL 里执行 JavaScript ——不用装 Node、不用起子进程,QuickJS 引擎直接内嵌进 DuckDB 进程。
● ● ●
为什么要在 SQL 里跑 JS
SQL 里总有 Lua/正则表达式够不着的逻辑。最典型的是 Unicode 属性正则 :
\p{L}
匹配任意语言的字母(含中文、日文假名),Lua 的 pattern 做不到——Lua 正则没有 Unicode 属性转义,遇到「提取一段文本里的所有中英文单词」这种需求只能写一堆字符区间。
还有现成的 JS 生态:day.js 日期处理、已有的前端/后端 JS 函数,想在 SQL 里复用。
● ● ●
三条路,实测对比
| 方案 | 每次调用开销 | 说明 |
|---|---|---|
子进程
node -e
|
~50–100 ms | 进程启动 + 数据序列化 |
| DuckDB 官方 python 扩展 | 重 | 要装 Python 运行时 |
| FFI 内嵌 QuickJS | 61.7 μs | 引擎在进程内,无序列化 |
libquickjs.so
只有
1.1 MB
,正好能被 duckdb-luajit 的
ffi.load
直接加载。
● ● ●
实现:三层,加起来不到 30 行
duckdb-luajit 内嵌 QuickJS 三层架构
第一层:QuickJS 源码编译 (1 条 make + 1 条 gcc,产出 1.1 MB 的 .so) 第二层:20 行 C 包装桥 。QuickJS 的
JSValue
是 16 字节结构体,直接跨 FFI 边界传递有 ABI 坑,所以用一个薄 C 桥消化所有细节,只暴露两个字符串函数:
const char *luaqjs_eval(const char *src); /* 执行 JS, 返回结果字符串 */第三层:SQL 里 3 行注册一个 UDF ,之后任何查询都能调:
void luaqjs_free(const char *s); /* 释放结果 */
LOAD 'luajit';
SELECT * FROM luajit_module(mode := 'compile', sql_name := 'qjs',
source := 'return function(x) ... ffi.load("luaqjs_bridge.so") ... end');
SELECT luajit_s('qjs', 'x'); -- 7
● ● ●
实测用例
① 算术 :
luajit_s('qjs', 'x')
→
7
(内部执行
1+2*3
)
② Unicode 属性正则(Lua 做不到的)
:
-- JS 源码: "Café 中文 123 テスト".match(/\p{L}+/gu).join(",")
SELECT luajit_s('jsre', 'x');
-- → Café,中文,テスト
一段混合
Café 中文 123 テスト
,
\p{L}
一行把英、中、日三种字母全部提出,数字被正确跳过。这个能力 Lua pattern 给不了。
③ JSON 结构化输出
:
JSON.stringify({name:"duckdb-luajit", version:0.3, tags:["lua","js"]})
→ 完整 JSON 字符串,可直接配 DuckDB 的
from_json
解析成列。
● ● ●
性能:57.9 μs / 次
10 万次调用(每次独立创建 QuickJS runtime、执行、释放), 10 万次全部成功,总耗时 5.8 秒 ,单次 57.9 μs 。对比子进程 node 的 50–100 ms,快了约 1000 倍。 同环境实测 DuckDB 社区扩展 quickjs(QuickJS-NG 引擎)做同样的 10 万次调用:单次 142–153 μs 。差异来自实现路径:社区扩展每行多做全局对象创建、console 注入、严格模式编译、DuckDB↔JS 值转换与 JSON 序列化(它的
quickjs_eval
支持参数绑定,功能更全);本文方案是字符串进出的最简桥。两者都每次新建 runtime,复用 runtime 是共同的优化空间。
(踩坑提醒:性能测试别用常量参数——DuckDB 优化器会把
WHERE udf('a','b')=...
折叠掉,测试要传列参数
udf('qjs', CAST(range AS VARCHAR))
,否则测的是优化器的速度不是你的 UDF。)
● ● ●
边界与下一步
-
QuickJS 是 ES2020+ 子集引擎:Promise、async、
\p{}正则、TypedArray 都支持; 没有 DOM、Node API。 - 当前每次调用新建 runtime(隔离最干净),复用 runtime 可再快一个数量级——这是给高频场景的优化方向。
- 下一篇预告 :SQL 里执行 Wasm——QuickJS 只解决 JS,Wasm 解决「任意语言」(Rust/Go/C 编译成 wasm32-wasi 都能在 SQL 里跑),且自带沙箱隔离。
● ● ●
参考来源
- 01 QuickJS 官方仓库(bellard/quickjs):https://github.com/bellard/quickjs
- 02 DuckDB 官方文档:https://duckdb.org/docs/
- 03 DuckDB 社区扩展 quickjs(本文对比对象):https://duckdb.org/community_extensions/extensions/quickjs
- 04 duckdb-luajit 扩展(作者本人项目,WSL2 本地源码路径 /mnt/d/wsl2/luajit)