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 |
|
require apple |
|
statement ok |
|
statement error |
|
query I |
|
---- |
|
apfs 扩展的 8 个测试文件
|
|
|
|
test_apfs_load.test |
|
|
test_apfs_scan_fts.test |
|
|
test_apfs_scan_spotlight.test |
|
|
test_apfs_search.test |
|
|
test_apfs_errors.test |
|
|
test_apfs_sql_integration.test |
|
|
test_apfs_has_changes.test |
|
|
test_apfs_scan_since.test |
|
|
| 合计 | 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 万文件):
|
|
|
|
|
|
|
|
|
|
|
|
|
|
22x |
|
|
|
|
77x |
差距来自三层:
-
1. 编译器优化: -O2启用内联、循环展开、SIMD -
2. D_ASSERT 断言:Debug 版每次 DataChunk 操作都检查,Release 中完全消失 -
3. 二进制体积:573MB 的 debug 版导致 I-Cache 命中率低
编译产物
|
|
|
|
|
|
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 扩展的版本兼容性是一个硬约束:
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 / 私有仓库)
扩展开发的三种模式
apfs 属于模式 1——核心模式是 Bind 解析参数 → Init 打开连接 → Execute 批量读取并填充 DataChunk。换成 HTTP API、gRPC、Redis,流程一样。
灵感清单:你的下一个扩展
|
|
|
|
| Docker 容器列表 |
|
|
| GitHub API 查询器 |
|
|
| 文件哈希计算 | md5(path)
|
|
| Redis 扫描器 |
|
|
| Kubernetes Pod 扫描器 | kubectl get pods
|
|
| Prometheus 指标查询 |
|
|
| macOS 日历/提醒事项 |
|
|
| 图片 EXIF 读取 |
|
|
| 日志文件解析 |
|
|
技术栈全景图
如果只能记住三件事:
-
1. DuckDB 表函数 = Bind + Init + Execute。掌握这个模型,你就能把任何数据源接入 SQL。 -
2. 扩展和应用分层。C++ 扩展做数据采集,上层应用(Go/Python/Web)做 UI 和业务逻辑。 -
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 行,扩展注册)
夜雨聆风