DuckDB-Wasm 扩展

Author Avatar
Carlo Piovesan
2023-12-18 · 8 分钟阅读

TL;DR: 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 标准语言。可以将其视为一种独立于机器的二进制格式,您可以在网页浏览器的沙箱环境中执行它。

得益于 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 数据集,这相当于 100 MB 的未压缩 CSV 格式数据。

脚本完成后,您可以继续执行查询,甚至可以使用以下命令下载 customer.parquet 文件(1 MB)

COPY customer TO 'customer.parquet';
.files download customer.parquet

这会首先将 customer.parquet 复制到 DuckDB-Wasm 文件系统,然后通过您的浏览器下载它。

简而言之,您的 DuckDB 实例(*完全在您的浏览器中运行*)首先安装并加载了TPC-H 扩展。然后,它使用扩展逻辑生成数据并将其转换为 Parquet 文件。最后,您可以将 Parquet 文件作为常规文件下载到本地文件系统。

Wasm shell using the TPC-H extension

在 DuckDB-Wasm 中使用空间扩展

为了展示 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 架构的概览。图中所示的两个组件都在网络浏览器中运行。

Overview of the architecture of DuckDB-Wasm

当您在浏览器中加载 DuckDB-Wasm 时,将设置两个组件:(1) 一个主线程包装库,它充当使用 DuckDB-Wasm 的用户或代码与驱动后台组件之间的桥梁。(2) 一个用于执行查询的 DuckDB 引擎。此组件位于 Web Worker 中,并通过消息与主线程组件通信。此组件有一个处理消息的 JavaScript 层,以及编译成单个 WebAssembly 文件的原始 DuckDB C++ 逻辑。

当我们加入扩展时会发生什么?

Overview of the architecture of DuckDB-Wasm with extensions

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 允许我们将 JavaScript 代码直接内联到 C++ 代码中。这意味着在运行时,当我们执行到该代码块时,WebAssembly 组件将返回到 JavaScript 环境,对诸如 https://extensions.duckdb.org/…/tpch.duckdb_extension.wasm 这样的 URL 执行一个阻塞的 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-HParquet空间扩展

得益于 DuckDB 的可移植性,这篇博客文章中展示的脚本同样可以在您的智能手机上运行。

Wasm shell using the TPC-H extension on iOS

如需了解最新进展,请关注此博客并加入我们的 Discord 中的 Wasm 频道。如果您有关于 DuckDB 扩展潜力的示例,请告诉我们!