DuckDB-Wasm 的扩展程序
简而言之:DuckDB-Wasm 用户现在可以加载 DuckDB 扩展程序,从而在浏览器中运行这些扩展功能。
在这篇博客中,我们将介绍两个令人兴奋的 DuckDB 特性:DuckDB-Wasm 客户端和 DuckDB 扩展程序。我将讨论这两个原本独立的功能现在是如何协同工作的。这些特性现已面向 DuckDB-Wasm 用户开放,您可以在 shell.duckdb.org 进行体验。
DuckDB 扩展
DuckDB 的理念是保持核心系统精简,以确保鲁棒性和可移植性。然而,另一个与之并行的设计目标是保持灵活性,并提供执行高级分析所需的一系列功能。为了实现这一点,DuckDB 拥有一个可以在运行时安装和加载扩展的机制。
在本地运行 DuckDB 扩展
对于 DuckDB,这是一个使用命令行界面的简单端到端示例。
INSTALL tpch;
LOAD tpch;
CALL dbgen(sf = 0.1);
PRAGMA tpch(7);
此脚本首先从官方扩展存储库安装 TPC-H 扩展(它实现了流行的 TPC-H 基准测试)。然后它加载 TPC-H 扩展,使用 dbgen 函数生成数据并填充数据库。最后,它运行 TPC-H 查询 7。
这个示例演示了安装扩展来补充 DuckDB 新功能(如 TPC-H 数据生成器)的情况,这些功能不属于基础 DuckDB 可执行文件的一部分。相反,它是从扩展存储库下载,然后在 DuckDB 框架内进行本地加载和执行。
目前,DuckDB 拥有多个扩展。它们增加了对文件系统、文件格式、数据库和网络协议的支持。此外,它们还实现了全文搜索等新功能。
DuckDB-Wasm
在 André Kohn 的带领下,DuckDB 于 2021 年被移植到 WebAssembly 平台。WebAssembly(也称为 Wasm)是近年来开发的一种 W3C 标准语言。您可以将其视为一种机器无关的二进制格式,可以在 Web 浏览器的沙箱中执行。
得益于 DuckDB-Wasm,任何人只需打开一个浏览器标签页即可访问 DuckDB 实例,所有计算都在浏览器内本地执行,且数据不会离开您的设备。DuckDB-Wasm 是一个可用于各种部署场景的库(例如:无需服务器即可在浏览器中运行的笔记本)。在这篇文章中,我们将使用 Web Shell,用户可以在其中逐行输入 SQL 语句,其行为模式参考了 DuckDB CLI Shell。
DuckDB-Wasm 中的 DuckDB 扩展!
DuckDB-Wasm 现已支持 DuckDB 扩展。此支持带来了四个新的关键特性。首先,DuckDB-Wasm 库可以编译以支持动态扩展;其次,DuckDB 扩展可以编译为单个 WebAssembly 模块;第三,使用 DuckDB-Wasm 的用户和开发者现在可以选择加载哪些扩展;最后,DuckDB-Wasm Shell 的功能现在更接近原生的 CLI 功能。
在 DuckDB-Wasm 中使用 TPC-H 扩展
为了演示这一点,我们将再次使用TPC-H 数据生成示例。要在您的浏览器中运行此脚本,请启动一个运行这些命令的在线 DuckDB Shell。该脚本将以 0.1 的比例因子生成 TPC-H 数据集,相当于未压缩 CSV 格式的 100 MB 数据。
脚本完成后,您可以继续执行查询,甚至可以使用以下命令下载 customer.parquet 文件 (1 MB):
COPY customer TO 'customer.parquet';
.files download customer.parquet
这将首先把 customer.parquet 复制到 DuckDB-Wasm 文件系统,然后通过浏览器下载它。
简而言之,您的 DuckDB 实例(完全在浏览器中运行)首先安装并加载了 TPC-H 扩展。然后,它使用扩展逻辑生成数据并将其转换为 Parquet 文件。最后,您可以将该 Parquet 文件作为普通文件下载到本地文件系统。
在 DuckDB-Wasm 中使用空间扩展 (Spatial Extension)
为了展示 DuckDB-Wasm 扩展带来的可能性并测试其能力,何不尝试在 DuckDB-Wasm 中使用 空间扩展?该扩展实现了地理空间类型和函数,使其能够处理地理空间数据及相关工作负载。
要在 DuckDB-Wasm 中安装并加载空间扩展,请运行
INSTALL spatial;
LOAD spatial;
使用空间扩展,以下查询使用了纽约出租车数据集,并计算了每个行政区出租车区域的面积:
CREATE TABLE nyc AS
SELECT
borough,
st_union_agg(geom) AS full_geom,
st_area(full_geom) AS area,
st_centroid(full_geom) AS centroid,
count(*) AS count
FROM
st_read('https://raw.githubusercontent.com/duckdb/duckdb-spatial/main/test/data/nyc_taxi/taxi_zones/taxi_zones.shp')
GROUP BY borough;
SELECT borough, area, centroid::VARCHAR, count
FROM nyc;
您的本地 DuckDB 客户端和在线 DuckDB Shell 都将执行相同的分析。
幕后原理
让我们深入了解其工作原理。下图展示了 DuckDB-Wasm 的架构概览。图中的两个组件均在 Web 浏览器内运行。

当您在浏览器中加载 DuckDB-Wasm 时,会设置两个组件:(1) 一个主线程包装器库,作为用户或使用 DuckDB-Wasm 的代码与后台组件之间的桥梁。(2) 一个用于执行查询的 DuckDB 引擎。此组件驻留在 Web Worker 中,并通过消息与主线程组件进行通信。该组件包含一个处理消息的 JavaScript 层,以及编译为单个 WebAssembly 文件的原生 DuckDB C++ 逻辑。
当我们添加扩展程序时会发生什么?

DuckDB-Wasm 的扩展程序由单个 WebAssembly 模块组成。它编码了扩展的逻辑和数据、将要导入和导出的函数列表,以及一段用于验证扩展的自定义元数据部分。
为了实现扩展加载,DuckDB 引擎组件会阻塞、获取并验证外部 WebAssembly 代码,然后将其链接起来,连接导入和导出。系统随后便会连接并准备就绪,仿佛它是单个代码库一样持续运行。
实现这一点的核心代码块如下:
EM_ASM(
{
const xhr = new XMLHttpRequest();
xhr.open("GET", UTF8ToString($0), false);
xhr.responseType = "arraybuffer";
xhr.send(null);
var uInt8Array = xhr.response;
// Check signatures / version compatibility left as an exercise
WebAssembly.validate(uInt8Array);
// Here we add the uInt8Array to Emscripten's filesystem,
// for it to be found by dlopen
FS.writeFile(UTF8ToString($1), new Uint8Array(uInt8Array));
},
filename.c_str(), basename.c_str()
);
auto lib_hdl = dlopen(basename.c_str(), RTLD_NOW | RTLD_LOCAL);
if (!lib_hdl) {
throw IOException(
"Extension \"%s\" could not be loaded: %s",
filename,
GetDLError()
);
}
在这里,我们依赖于 Emscripten 的两个强大特性,这是我们用于将 DuckDB 编译为 WebAssembly 的编译器工具链。
首先,EM_ASM 允许我们在 C++ 代码中直接内联 JavaScript 代码。这意味着在运行时执行到该代码块时,WebAssembly 组件将返回 JavaScript 环境,在 URL(如 https://extensions.duckdb.org/…/tpch.duckdb_extension.wasm)上执行阻塞式的 XMLHttpRequest,然后验证刚刚获取的包是否确实是一个有效的 WebAssembly 模块。
其次,我们利用 Emscripten 的 dlopen 实现,这使得兼容的 WebAssembly 模块能够被链接在一起,并作为一个单一的可组合代码库运行。
当通过 SQL LOAD 语句触发时,这些特性实现了扩展的动态加载。
开发者指南
我们看到两类主要开发者在使用 DuckDB-Wasm 的扩展:
- 使用 DuckDB-Wasm 的开发者:如果您正在构建一个网站或包装 DuckDB-Wasm 的库,新的扩展支持意味着现在有更广泛的功能可以提供给您的用户。
- 开发 DuckDB 扩展的开发者:如果您已经编写了 DuckDB 扩展,或者正考虑这样做,请考虑将其移植到 DuckDB-Wasm。DuckDB 扩展模板存储库中包含了编译到 DuckDB-Wasm 所需的配置。
限制
DuckDB-Wasm 扩展有一些固有的限制。例如,无法与您机器上的原生可执行文件进行通信,而某些扩展(如 postgres 扫描器扩展)需要这样做。此外,您所依赖的某些库可能目前不支持编译为 Wasm;由于浏览器施加的额外要求(特别是关于 非安全 HTTP 请求 的要求),其功能可能与本地可执行文件不完全一致。
结论
在这篇博客中,我们解释了 DuckDB-Wasm 如何支持扩展,并演示了多个扩展:TPC-H、Parquet 和 spatial(空间)。
得益于 DuckDB 的可移植性,本文展示的脚本在您的智能手机上也同样适用。

如需获取最新进展,请关注本博客并加入我们 Discord 中的 Wasm 频道。如果您有关于 DuckDB 扩展功能的示例,请告诉我们!
