乐于分享
好东西不私藏

DuckDB 插件开发实战:Mac 版 Everything–测试、编译、分发与开发 Checklist

DuckDB 插件开发实战:Mac 版 Everything–测试、编译、分发与开发 Checklist

测试、编译、分发与开发 Checklist

—— 从代码到用户手里的最后一公里

本文是《DuckDB 插件开发实战:Mac 版 Everything》系列的第 6 篇,也是最后一篇。写完代码只是一半,另一半是测试、编译、分发。这篇走完整个链路,最后给你一份可带走的开发 Checklist。

版本基线:本文所有行数、断言数、性能数据均基于 duckdb-apfs 仓库 duckdb/ submodule 锁定的 DuckDB v1.5.2、macOS 15.6 arm64(M2 MacBook Pro 16GB)实测。sqllogictest 8 个文件共 574 行 / 74 断言,运行耗时约 3m52s(全绿)。Debug/Release 二进制体积(43MB / 573MB)为 Linux x86_64 release build 的参考值;实测 arm64 上仅 apfs 扩展动态库本身 7.0M,CLI 整体取决于 DuckDB 构建配置。所有”实测”数据为作者环境实测,读者环境会有差异。
仅基本功能实现,实际运行体验仍与Windows版本有不小差距,源码见参考资料。


测试:sqllogictest

DuckDB 用的不是 gtest,而是自己的 sqllogictest 框架——用 SQL 写测试。

测试文件格式

# name: test/sql/apfs/test_apfs_load.test
# description: Test that apfs extension loads correctly
group: [apfs]

require apfs
require apple

# 扩展已加载
statement ok
SELECT*FROM duckdb_extensions() WHERE extension_name ='apfs'AND loaded =true;

# apfs_scan 函数存在
query I
SELECTCOUNT(*>=0FROM apfs_scan('/tmp''fts') LIMIT 0;
----
true

几个关键语法:

语法
含义
require apfs
需要 apfs 扩展已加载
require apple
只在 macOS 上运行
statement ok
执行 SQL,期望成功
statement error
执行 SQL,期望报错
query I
查询返回 1 列整数
----
分隔符,下面是期望结果

apfs 扩展的 8 个测试文件

文件
行数
测试内容
test_apfs_load.test
42
扩展加载、函数注册
test_apfs_scan_fts.test
124
fts 模式扫描、列 schema、深度
test_apfs_scan_spotlight.test
67
spotlight 模式扫描
test_apfs_search.test
83
关键词搜索
test_apfs_errors.test
64
无效路径、无效模式、参数错误
test_apfs_sql_integration.test
130
GROUP BY / ORDER BY / WHERE 集成
test_apfs_has_changes.test
41
变更检测函数
test_apfs_scan_since.test
23
增量扫描参数
合计 574 74 个断言

测试示例:SQL 集成

# test_apfs_sql_integration.test

# 先创建测试文件
statement ok
COPY (SELECT'aaaa'TO'__TEST_DIR__/doc1.pdf' (FORMAT CSV, HEADER false, QUOTE '');

statement ok
COPY (SELECT'ccc'TO'__TEST_DIR__/image.png' (FORMAT CSV, HEADER false, QUOTE '');

# 扫描并建表
statement ok
CREATE TABLE files ASSELECT*FROM apfs_scan('__TEST_DIR__''fts');

# 按扩展名分组
query II
SELECT extension, COUNT(*as cnt FROM files
WHERE file_type ='file'
GROUPBY extension ORDERBY extension;
----
cpp    1
md    1
pdf    1
png    1

__TEST_DIR__ 是 sqllogictest 提供的临时目录,每次测试自动创建和清理——不会污染真实文件系统。

运行测试

# 编译 debug 版
make debug

# 运行全部 apfs 测试
build/debug/test/unittest "test/sql/apfs/*"

# 运行单个测试
build/debug/test/unittest "test/sql/apfs/test_apfs_scan_fts.test"

Go 测试

Web UI 的 Go 代码也有完整测试:

cd mac-everything
go test -v -timeout 300s

测试覆盖 API 端点、搜索逻辑、排序、错误处理、文件监听、增量更新等。


编译

Debug vs Release

make debug    # -O0, 断言开启, 573MB, 开发用
make          # -O2, 断言关闭,  43MB, 生产用

# 加速编译
GEN=ninja make debug
BUILD_EXTENSIONS="apfs" make debug

实测性能差距(M2 MacBook Pro 16GB,10.5 万文件):

操作
Debug
Release
加速比
建表
7.79s
1.84s
4.2x
ILIKE 搜索
0.427s
0.019s
22x
聚合查询
0.774s
0.010s
77x

差距来自三层:

  1. 1. 编译器优化-O2 启用内联、循环展开、SIMD
  2. 2. D_ASSERT 断言:Debug 版每次 DataChunk 操作都检查,Release 中完全消失
  3. 3. 二进制体积:573MB 的 debug 版导致 I-Cache 命中率低

编译产物

产物
Debug 路径
Release 路径
DuckDB CLI(内含扩展)
build/debug/duckdb build/release/duckdb
动态扩展文件
build/debug/extension/apfs/apfs.duckdb_extension build/release/extension/apfs/apfs.duckdb_extension
测试运行器
build/debug/test/unittest

分发

方式 1:分发 DuckDB CLI(推荐)

make                              # 编译 release
cp build/release/duckdb ./        # 一个 43MB 的文件
./duckdb -c "SELECT * FROM apfs_search('test') LIMIT 5;"

最简单。用户拿到一个文件,直接用。扩展已经静态链接进去了。

方式 2:分发 .duckdb_extension

-- 用户在自己的 DuckDB 中加载
SET allow_unsigned_extensions =true;
LOAD '/path/to/apfs.duckdb_extension';
SELECT*FROM apfs_search('test') LIMIT 5;

限制:版本必须严格匹配、未签名需要 allow_unsigned_extensions、平台匹配(arm64 和 x86_64 不通用)。

方式 3:Mac Everything 一体化

# 打包分发(包含 Go 二进制 + DuckDB CLI)
make dist DUCKDB_BIN=/path/to/duckdb-with-apfs

# 用户解压即用
tar xzf mac-everything-v1.0-darwin-arm64.tar.gz
./mac-everything
# 自动打开浏览器 http://localhost:8999

CI/CD

extension-ci-tools/ 提供了 GitHub Actions 的标准配置:

jobs:
build:
runs-on:macos-latest# apfs 只能在 macOS 上编译
steps:
-uses:actions/checkout@v4
with:
submodules:recursive
-run:make# 编译 release
-run:maketest# 运行测试

注意:apfs 扩展的 CI 只能跑在 macOS runner 上——因为 CoreFoundation 和 CoreServices 框架只有 macOS 有。


版本兼容性

DuckDB 扩展的版本兼容性是一个硬约束:

v1.5.0

v1.4.0

编译时DuckDB v1.5.0 + apfs
apfs.duckdb_extensionv1.5.0
加载时 DuckDB 版本?
✅ 可以加载
❌ 版本不匹配,拒绝

DuckDB 内部的 C++ ABI 在不同版本间可能变化。动态加载时会检查入口函数中嵌入的版本号,不匹配就拒绝。

解决办法:用 duckdb/ git submodule 锁定版本——编译扩展和用户使用的 DuckDB 是同一个版本。


开发 Checklist

阶段 1:脚手架

  • • 基于 extension-template 创建项目
  • • 配置 extension_config.cmake(扩展名 + LOAD_TESTS
  • • 配置 CMakeLists.txt(源码列表 + 系统库链接)
  • • make debug 编译通过
  • • 实现一个最简函数(比如 hello_world()),在 CLI 中验证

阶段 2:核心功能

  • • 定义输出列 schema(return_types + names
  • • 实现 Bind:解析参数 + 返回 BindData
  • • 实现 Init:打开数据源 + 返回 GlobalState
  • • 实现 Execute:循环填充 DataChunk(每批最多 2048 行)
  • • 在 LoadInternal 中注册函数
  • • 实现 C 入口点(DUCKDB_CPP_EXTENSION_ENTRY
  • • GlobalState 析构函数释放资源

阶段 3:测试

  • • 编写 sqllogictest 测试文件
    • • 扩展加载测试(require your_extension
    • • 正向功能测试(列 schema、数据正确性)
    • • 错误处理测试(无效参数、不存在的路径)
    • • SQL 集成测试(WHERE / GROUP BY / ORDER BY)
  • • make test 全部通过

阶段 4:发布

  • • make(release 编译)通过
  • • 测试 release 版的查询性能
  • • 确认分发方式(CLI 二进制 / .duckdb_extension / 私有仓库)

扩展开发的三种模式

把外部数据接入 SQL

添加自定义计算

支持新的存储格式

你要做什么?
模式 1:外部数据源扫描TableFunction
模式 2:自定义函数ScalarFunction
模式 3:存储后端FileSystem / CopyFunction
apfs / httpfs / postgres_scannersqlite_scanner / delta / excel
icu / fts / json 函数
parquet / httpfs / motherduck

apfs 属于模式 1——核心模式是 Bind 解析参数 → Init 打开连接 → Execute 批量读取并填充 DataChunk。换成 HTTP API、gRPC、Redis,流程一样。


灵感清单:你的下一个扩展

方向
数据源
难度
Docker 容器列表
Docker API → SQL 表
GitHub API 查询器
GitHub REST API → SQL 表
文件哈希计算 md5(path)

 标量函数
Redis 扫描器
SCAN 命令 → SQL 表
Kubernetes Pod 扫描器 kubectl get pods

 → SQL 表
Prometheus 指标查询
PromQL → SQL 表
macOS 日历/提醒事项
EventKit API → SQL 表
图片 EXIF 读取
libexif → SQL 表
日志文件解析
自定义格式 → 结构化表

技术栈全景图

Mac 版 Everything 技术栈

HTTP

CLI 子进程

浏览器前端index.html ~1000 行
Go 服务端~1700 行
DuckDB + apfs 扩展C++ ~1200 行
HTTP API
持久化索引
fsnotify 监听
配置热加载
apfs_scan fts
apfs_scan spotlight
apfs_search
apfs_has_changes
fts(3) POSIX
MDQuery Spotlight

如果只能记住三件事:

  1. 1. DuckDB 表函数 = Bind + Init + Execute。掌握这个模型,你就能把任何数据源接入 SQL。
  2. 2. 扩展和应用分层。C++ 扩展做数据采集,上层应用(Go/Python/Web)做 UI 和业务逻辑。
  3. 3. 测试优先。sqllogictest 让你用 SQL 写测试——比 C++ 单元测试简洁得多。

DuckDB 扩展开发的门槛比你想象的低。 一个 180 行的入口文件,一个 184 行的扫描引擎,6 行的 CMake 注册——就够了。

去写你的扩展吧。


参考资料

  • • mac-everything
  • • duckdb-apfs
  • • DuckDB sqllogictest 文档
  • • DuckDB Extension Template
  • • DuckDB 官方文档 – Extensions
  • • DuckDB 社区扩展列表
  • • duckdb-apfs 源码:test/sql/apfs/(8 个测试文件,574 行,74 个断言)
  • • duckdb-apfs 源码:CMakeLists.txt(37 行,构建配置)
  • • duckdb-apfs 源码:extension_config.cmake(6 行,扩展注册)