- 数据库
- OLAP
- 嵌入式数据库
- 数据分析
【免费下载链接】duckdb
DuckDB is an analytical in-process SQL database management system
DuckDB 的 C API 在 api_spec/VERSIONING.md 中定义了一套完整的声明式版本化方案:所有 API 符号(函数、类型、回调)都带有一个按日期堆叠的生命周期(lifecycle),并据此决定何时出现在duckdb.h中、何时稳定、何时弃用或移除。本文从生命周期状态机讲起,依次说明直接链接libduckdb的消费者如何用版本宏做"按版本取 API"的版本门控,以及 C 扩展如何通过版本化 v-table 结构实现 ABI 前缀兼容、向前加载与固定版本锁定,最后解析重命名、移除与版本宏转发等边界规则,并给出仓库内的源码与脚本依据。
为什么需要一套"声明的"版本化方案
DuckDB 的 C API 有两个头文件:面向直接消费者的 src/include/duckdb.h,以及面向 C 扩展开发者的 src/include/duckdb_extension.h。这两个头文件都不是手写的,而是由 api_spec/v1/ 下的 YAML 规范经 capigen 生成器统一生成(见 scripts/capi_v1_regen.sh 与 api_spec/README.md)。
这一设计带来一个关键好处:两个头文件共享同一条记录在规范中的生命周期历史,因此二者永远不会对"某个符号何时出现、何时稳定"产生分歧。直接使用duckdb.h的消费者和通过duckdb_extension.h使用扩展 API 的开发者,面向"某个版本"取到的 API 是一致的;唯一的差别在于扩展还额外要求固定的 ABI 布局(v-table 中函数指针的槽位),这一点下文会专门展开。
生命周期(Lifecycle):每个符号的四态历史
规范中每一个 API 条目都携带一叠带日期的状态迁移记录,最新的状态在最上面:
lifecycle: - [ "stable", "v1.5.6", "2026-07-30" ] - [ "unstable", "v1.4.0", "2025-09-12" ]当前共有四种状态,语义如下:
| 状态 | 含义 |
|---|---|
unstable | 已存在但未承诺;可能在任何时候改变甚至消失 |
stable | 已承诺;从此冻结签名与 ABI 槽位 |
deprecated | 仍承诺可用,但已排入移除计划;调用方应迁移 |
removed | 已从库中消失 |
仓库中的真实生命周期示例随处可见。例如 api_spec/v1/common/types.yaml 中blob类型在v0.2.5(2021-03-10)稳定,bit类型在v1.2.0(2025-02-05)稳定;api_spec/v1/arrow/arrow.yaml 中多个 Arrow 符号在v1.0.0(2024-05-29)被标记为deprecated,api_spec/v1/appender/appender.yaml 中也有符号在v1.4.0(2025-09-15)弃用。
在 api_spec/v1/metadata.yaml 中,四种状态的可见性策略被显式声明:
lifecycle_states: stable: visibility: always deprecated: visibility: opt_out guard: DUCKDB_API_NO_DEPRECATED unstable: visibility: opt_in guard: DUCKDB_EXTENSION_API_VERSION_UNSTABLE这里always(始终可见)、opt_out(默认可见、可关闭)、opt_in(默认隐藏、需显式打开)直接对应下文要讲的版本宏默认值。值得注意的是 metadata 中的注释说明:当前已没有处于unstable的符号,此前所有通过DUCKDB_EXTENSION_API_VERSION_UNSTABLE选择加入的符号都已稳定进 v1.5.6;但该状态仍被保留,用于支撑 174 条经过它的历史——这正是让目标是 v1.5.6 之前版本的消费者能够重新选择加入这些符号的机制。
每个函数都必须声明生命周期。原因在于:函数在扩展 v-table 中的槽位由它稳定时的版本(若未稳定,则在末尾的 unstable 段)决定,一个没有日期的函数因此不存在确定的槽位位置。这也是"版本化"能落地的根本前提。
直接消费者(duckdb.h):用宏做版本门控
客户端库或直接链接libduckdb的应用程序属于此类。对于它们,声明本身没有必须保持的内在顺序,因此可以按符号独立进行版本门控。控制可见符号的宏有三个:
DUCKDB_API_VERSION_MAJOR/_MINOR/_PATCH:要瞄准的 API 版本,默认取头文件描述的最新版本(当前为 1.5.6,见 src/include/duckdb.h)。三个必须全定义或全不定义。声明只有在"截至该版本已稳定"时才出现,即"给我版本 X 时的 API"。DUCKDB_API_ALLOW_DEPRECATED:默认1。设为0后,任何相对你的目标版本已弃用的符号都会消失,编译器就能帮你找出剩余的所有旧用法。弃用是相对目标而言的:一个在 v1.5.6 弃用的符号,对瞄准 v1.5.4 的构建仍然可见。DUCKDB_API_ALLOW_UNSTABLE:默认0。设为1可显示尚未稳定的符号,代价是接受它们可能变化。这要求目标必须是最新版本(原因见下节)。
为向后兼容,旧的DUCKDB_API_NO_DEPRECATED与DUCKDB_EXTENSION_API_VERSION_UNSTABLE宏仍然有效,它们只是分别设置新的DUCKDB_API_ALLOW_DEPRECATED/DUCKDB_API_ALLOW_UNSTABLE宏(见 src/include/duckdb.h 的默认值推导逻辑)。
Unstable 本身是一个"独立的版本"
一个符号从它被稳定的版本开始对外发布,而不是从它被引入的版本。在稳定之前,它不属于任何已发布的版本,签名仍可改变。如果既要求旧目标版本、又要求 unstable 表面,就会在"该版本从未承诺过的名字"下拿到今天的签名——这正是"版本 X 时的 API"唯一不成立的场景。因此二者互斥:
#if DUCKDB_API_ALLOW_UNSTABLE && !DUCKDB_API_VERSION_AT_LEAST(1, 5, 6) #error "the unstable surface requires targeting the newest API version" #endif这段保护逻辑就实实在在写在生成的 src/include/duckdb.h 中。它同样被DUCKDB_EXTENSION_API_VERSION_AT_LEAST版本的语义继承。
由此,对两类消费者都只留下一条规则:
- 选一个版本:拿到截至该版本已稳定的表面,签名冻结、弃用状态相对该版本;
- 选 unstable:拿到最新版本的全部符号(含不稳定符号),不提供任何承诺。
因此,任何门控(#if)永远只读取DUCKDB_API_VERSION_AT_LEAST(<稳定版本>),或者对尚未稳定的符号使用裸开关(DUCKDB_API_ALLOW_UNSTABLE)。
扩展(duckdb_extension.h):版本化的 v-table 与 ABI
扩展与直接消费者有本质区别:非静态链接的扩展并不直接链接引擎符号。它收到一个函数指针结构体(v-table),所有调用都经由一组"间接层宏"(indirection macros)穿透该结构。扩展加载时,这个结构体会被拷贝进一个全局静态变量:
duckdb_ext_api = *res; /* copies sizeof(the extension's struct) */对应的初始化宏DUCKDB_EXTENSION_API_INIT定义在扩展头模板 api_spec/v1/extension/duckdb_extension.h.in 中:它通过access->get_api(info, minimum_api_version)拿到结构体指针并逐字节拷贝。
这个拷贝之所以成立,前提是扩展期望的结构体必须是 DuckDB 实际传入结构体的前缀(prefix)——因此结构体的布局是 ABI 的一部分,不能依赖任何引擎看不到的东西。这正是 scripts/check_extension_abi.py 存在的原因。
v-table 结构体按版本分带(band)
v-table 中的函数按被稳定的版本(而非被引入的版本)划分成连续的带(band),每个带只在单一门控下发出:
typedef struct { /* band v1.2.0 — 404 slots, always present */ #if DUCKDB_API_VERSION_AT_LEAST(1, 5, 6) /* band v1.5.6 — 142 slots */ #endif #if DUCKDB_API_ALLOW_UNSTABLE /* any future unstable functions, always at the end */ #endif } duckdb_ext_api_v1;瞄准旧版本因此会把结构体截断到该版本当时实际发布的槽位数量。由于 Extension-C-API 结构体在v1.2.0才首次引入(v-table 中 v1.2.0 带始终存在,共 404 个槽位;v1.5.6 带 142 个槽位),任何早于它的目标都会是编译错误而非空结构体——这一保护也写在模板中(见 api_spec/v1/extension/duckdb_extension.h.in)。
scripts/check_extension_abi.py负责针对每个发布标签验证这一前缀性质:它用cc -E -DDUCKDB_EXTENSION_API_VERSION_MAJOR=...预处理出"固定到某版本"的槽位列表,再与各 release tag 上实际随引擎发布的结构体对比,确保每个被该引擎接受的扩展 pin 都是引擎结构体的前缀(见 scripts/check_extension_abi.py 与主循环 scripts/check_extension_abi.py)。注意脚本需要本地存在 release tag,缺失时会提示跳过并以 0 退出,避免浅克隆误报失败。
同样是两种选择,这次由构建系统替你选
上一节"Unstable 是一个独立的版本"的规则在扩展场景完全适用:选版本,或选 unstable。区别在于:扩展自己不设置宏,且这个选择不仅决定它能看见哪些符号,还决定哪些 DuckDB 版本能加载它。所以由 CMake 构建辅助函数来做决定:
Versioned(版本化):可向前加载(forward-loadable)
build_loadable_extension_capi(my_ext 1 5 6 ${SOURCES})ABI 类型为C_STRUCT。你声明所需的最低 API 版本(这隐式设置了DUCKDB_EXTENSION_API_VERSION_*宏)。任何等于或高于该版本的 DuckDB 都能加载你的扩展;你拿到截至目标版本的 v-table 前缀,不会更多。要点:
- 钉住"包含所需功能的最低版本":v1.2.0 与 v1.5.6 之间没有新符号稳定,因此中间任何一个 pin 都产生同样的 404 个槽位,却白白排除了更早的 DuckDB;
DUCKDB_API_ALLOW_UNSTABLE按前述规则不可用;DUCKDB_API_ALLOW_DEPRECATED仍然有效:禁用它去掉的是名字(间接层宏),而不是槽位,因此可以在不扰动 v-table 布局的前提下隐藏符号。
Pinned(固定):锁定到特定 DuckDB 版本
build_loadable_extension_capi_unstable(my_ext ${SOURCES})ABI 类型为C_STRUCT_UNSTABLE。你不能设置任何版本宏;你拿到完整的 v-table 结构体,外加尚未稳定的尾部。DuckDB 只会在与你构建时完全相同的版本中加载该扩展——这一约束由扩展 metadata footer 中记录的版本来强制。这种构建向get_api报告的是确切的版本身份(release tag,或开发构建的 git commit hash),而非语义化版本,该值取自构建系统设置的DUCKDB_EXTENSION_API_VERSION_UNSTABLE(见模板中的DUCKDB_EXTENSION_API_VERSION_STRING推导,api_spec/v1/extension/duckdb_extension.h.in)。
为什么不能既要 unstable 符号又要钉版本?
通用原因前面已说:不稳定的签名未冻结,任何版本都无法承诺它。但对扩展还有一个额外的重要原因:unstable 尾部位于所有版本带之后,其槽位偏移依赖前面所有稳定版本带先编译进来。若一个扩展钉住 v1.2.0 却又编译了尾部,它的第一个尾部槽位会落在索引 404,而 DuckDB 实际放在 546——每一次 unstable 调用都会静默地穿过错误的指针。duckdb.h已发出的互斥守卫(#error)正是为了阻止这种情况。
这在实践中没有任何代价:唯一能触达尾部的构建本来就被锁定到特定 DuckDB 版本。
这也再次确认:槽位在符号"稳定"时冻结,而非"引入(unstable)"时冻结。函数处于 unstable 期间,只会被锁定到单一版本的构建观察到,因此仍可被重排、改签名或整体删除;一旦设为stable,槽位永久固定。对 C-API 新增函数的工程含义是:一个开发中发现有问题的函数,必须在它的stable版本发布之前修好,否则将永久占据一个槽位。
重命名(Renames):保持 ABI、放弃源码兼容
应尽可能避免重命名函数,但仓库历史上已经发生过几次。重命名的符号保留槽位与签名,只改变拼写,因此 ABI 兼容但不源码兼容。规范中这样记录:
create_bignum: renamed_from: { name: create_varint, version: "v1.4.0" }仓库真实案例见 api_spec/v1/value/value.yaml(create_varint→create_bignum)、api_spec/v1/value/value.yaml(get_varint→get_bignum)以及 api_spec/v1/common/types.yaml(类型varint→bignum,均为 v1.4.0)。
旧拼写随后以门控在该版本BELOW的别名发出:类型用typedef、函数用#define。这样它只在"你瞄准的版本仍包含它"时可见,一旦瞄准了执行重命名的版本就消失。这些别名存放在duckdb.h中;扩展头包含它之后,其自身的映射宏链会穿过该别名,因此重命名后的函数仍通过 v-table 解析,而不是解析到库符号。
scripts/check_extension_abi.py直接从生成的头文件的// Renamed constructs兼容区读回这些别名(见 scripts/check_extension_abi.py),在对比前把旧名映射回新名——这样一次重命名不会被误读为"后续所有槽位都发生了位移",且未来再有重命名也无需改动检查脚本。
移除(Removal):槽位保留、名字消失
一个removed函数必须保留它的槽位,以免后续偏移发生移动,但它会失去映射宏,因此名字不再编译。DuckDB 可以自由地把对应函数指针留为NULL。
需要特别强调的是:移除是唯一能破坏"已经构建好的"扩展的操作——旧扩展的槽位索引已固化,遇到更新的 DuckDB 时该槽位是NULL,调用会直接崩溃(而非编译失败)。版本化机制对此无能为力,因为扩展的编译早于移除动作。因此文档给出的工程建议是:优先选择弃用(deprecation)——它在运行时零成本:槽位保持填充、旧二进制继续工作、新构建得到编译错误。
版本宏转发(Forwarding):让两个头文件始终锁步
duckdb_extension.h在包含duckdb.h之前,先解析DUCKDB_EXTENSION_API_VERSION_*并转发为DUCKDB_API_VERSION_*:
- 若直接消费者宏未定义,则把
DUCKDB_EXTENSION_API_VERSION_MAJOR/MINOR/PATCH原样转发(见 api_spec/v1/extension/duckdb_extension.h.in); - 若两者都被显式定义且不一致,则报错(
#error,api_spec/v1/extension/duckdb_extension.h.in)。
若不转发,duckdb.h会声明最新的表面,而映射宏却跟随扩展的旧目标:一个映射宏缺失的名字会悄悄解析到duckdb.h里的真实声明,使可加载扩展直接引用引擎符号——这正是扩展绝不允许发生的事。转发使二者保持锁步,这样的名字会变成编译错误。
实战:如何选择与验证
综合全文,直接消费者与扩展开发者的决策路径可以总结为:
- 直接消费者:默认用最新头文件;需要兼容旧引擎时,定义全部三个
DUCKDB_API_VERSION_*宏瞄准旧版本;需要强制淘汰旧用法时,设DUCKDB_API_ALLOW_DEPRECATED=0;只有明确接受"无承诺"时才设DUCKDB_API_ALLOW_UNSTABLE=1(且必须瞄准最新版本)。 - 扩展开发者:尽量选择 versioned 构建(
build_loadable_extension_capi),钉住所需功能的最低稳定版本以获得最大兼容面(向前加载);只有需要尚未稳定的新符号时才使用 pinned 构建(build_loadable_extension_capi_unstable),并接受"只能被完全相同版本加载"的限制。 - 验证 ABI:在本地包含 release tag 的仓库中运行
python3 scripts/check_extension_abi.py [--cc cc],确认每个 pin 都是所有接受它的引擎结构体的合法前缀(脚本用法见 scripts/check_extension_abi.py)。 - 新增函数:按 api_spec/README.md 的流程,在模块 YAML 中声明带
lifecycle的条目(必要时把新版本加入 api_spec/v1/metadata.yaml 的versions),再通过make generate-files(或./scripts/capi_v1_regen.sh、uv run --project api_spec --group generate ./scripts/capi_v1_regen.sh,需 Python 3.12+)重新生成头文件;CI 会校验提交的头文件与规范一致。
这套从"单一规范源"到"双头文件生成"再到"ABI 前缀校验脚本"的闭环,正是 DuckDB C API 能同时服务直接消费者与扩展生态、并在多年版本演进中保持稳定 ABI 的基础设施。
- 数据库
- OLAP
- 嵌入式数据库
- 数据分析
【免费下载链接】duckdb
DuckDB is an analytical in-process SQL database management system
相关推荐
SponsorBlock扩展版本控制策略:语义化版本与发布周期
SponsorBlock扩展版本控制策略:语义化版本与发布周期 你是否曾困惑于浏览器扩展的版本号究竟代表什么含义?为什么有时更新仅修复小问题,有时却带来全新功能
前端Ollama版本控制策略:语义化版本与发布周期深度解析
Ollama版本控制策略:语义化版本与发布周期深度解析 想要在本地高效运行Llama 2等大型语言模型?Ollama作为业界领先的本地AI模型管理工具,其精心设
人工智能大模型模型推理服务本地部署Azure Linux容器镜像生命周期:版本控制与清理策略
Azure Linux容器镜像生命周期:版本控制与清理策略 为什么容器镜像管理至关重要? 在云原生应用部署中,容器镜像的生命周期管理直接影响系统稳定性、安全性和
操作系统云原生容器
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考