TL;DR:尽管 DuckDB 核心代码没有外部依赖,但构建带有依赖的扩展现在变得非常简单,内置支持 vcpkg,这是一个支持超过 2000 个 C/C++包的开源包管理器。有兴趣自己构建扩展吗?请查看扩展模板。
引言
自 DuckDB 诞生以来,其核心理念之一就是严格遵循“无外部依赖”的原则。引用 2019 年 SIGMOD 论文对 DuckDB 的描述:
为了实现“可嵌入性”和“可移植性”的要求,数据库需要能够在宿主系统运行的环境中运行。依赖外部库(例如 openssh)无论是编译时还是运行时都被发现是存在问题的。
在本博客文章中,我们将探讨 DuckDB 如何在不强迫开发者完全放弃依赖的情况下,坚持这一理念。我们将通过实际示例展示如何实现外部依赖,并展示如何在创建自己的 DuckDB 扩展时使用这些方法。
完全禁欲的困难
完全不使用外部依赖在概念上非常简单,但在现实世界中,要实现这一目标却非常困难。许多功能需要复杂的协议和算法实现,而许多高质量的库已经实现了这些功能。这意味着对于 DuckDB(以及其他大多数系统)来说,处理可能涉及外部依赖的需求基本上有三种选择:
- 内联外部代码
- 重写外部依赖
- 破坏无依赖规则
前两种选择非常直接:为了避免依赖某些外部软件,只需将其纳入代码库。通过这样做,依赖外部代码的不可预测性就被消除了!DuckDB 已经通过内联和重写来防止依赖。例如,Postgres 解析器和 MbedTLS 库被内联到 DuckDB 中,而对 S3 的支持则是通过自定义实现 AWS S3 协议来提供的。
好吧,问题解决了吗?当然没有。大多数有软件工程经验的人都会意识到,内联和重写都存在严重的缺点。最根本的问题可能与代码维护有关。每个重要的软件都需要一定程度的维护,从修复错误到应对不断变化的(构建)环境或需求,代码需要修改以保持功能性和相关性。当内联/重写依赖时,这也意味着复制了维护负担。
对于 DuckDB 来说,这意味着对于每个依赖项,都会非常谨慎地权衡增加的维护负担与依赖的必要性。包含一个依赖项意味着要承担维护它的责任,因此这个决定从未被轻易做出。在许多情况下,这可以很好地工作,并且还有一个额外的好处,即迫使开发者批判性地思考是否要包含一个依赖项,而不是盲目地一个接一个地添加库。然而,对于某些依赖项,这根本行不通。例如,大型云服务提供商的 SDK 通常非常庞大、更新频繁,并且包含了一个日益成熟的分析数据库中不可或缺的功能。这就留下了一个尴尬的选择:要么不提供这些基本功能,要么打破无依赖规则。
DuckDB 扩展
扩展在这里发挥了作用。扩展通过允许在不破坏无依赖规则的情况下,细粒度地引入依赖,从而优雅地解决了依赖的困境。将依赖从 DuckDB 核心移出到扩展中,核心代码库可以保持无依赖。这意味着 DuckDB 的“实用可嵌入性和可移植性”不会受到威胁。另一方面,DuckDB 仍然可以提供那些不可避免需要依赖第三方库的功能。此外,通过将依赖移到扩展中,每个扩展可以对依赖的不稳定性有不同的暴露程度。例如,一些扩展可能选择只依赖于高度成熟、稳定的库,这些库具有良好的可移植性,而其他扩展可能选择包含更多实验性的依赖,这些依赖的可移植性有限。然后,这种选择通过允许用户选择使用哪个扩展来传递给用户。
在 DuckDB 中,扩展的重要性以及它与无依赖规则的关系很早就被认识到,因此可扩展性从早期就融入了 DuckDB 的设计中。如今,DuckDB 的许多部分都可以扩展。例如,你可以添加函数(表函数、标量函数、复制函数、聚合函数)、文件系统、解析器、优化器规则等。许多新功能是通过扩展添加到 DuckDB 中的,并且根据功能或依赖集进行分组。一些扩展的例子包括用于读取/写入 SQLite 文件的 SQLite 扩展,以及提供广泛地理空间处理功能支持的 Spatial 扩展。DuckDB 的扩展作为可加载的二进制文件分发,支持大多数主要平台(包括 DuckDB-Wasm),允许通过两条简单的 SQL 语句加载和安装扩展:
INSTALL spatial;
LOAD spatial;
对于大多数由 DuckDB 团队维护的核心扩展,甚至有一个自动安装和自动加载功能,它将检测 SQL 语句所需的扩展并自动安装和加载它们。关于可用扩展及其使用方法的详细描述,请查看文档。
依赖管理
到目前为止,我们已经看到了 DuckDB 如何通过将依赖从核心代码库移出到扩展中,避免核心代码库的外部依赖。然而,我们还没有走出困境。由于 DuckDB 是用 C++编写的,最自然的扩展编写方式也是 C++。在 C++中,没有标准的工具,比如包管理器,多年来关于如何在 C++中进行依赖管理的问题的答案一直是:“通过极大的痛苦和折磨。”鉴于 DuckDB 注重可移植性和对许多平台的支持,手动管理依赖是不可行的:依赖项通常是从源代码构建的,每个依赖项都有其自身的复杂性,需要为不同平台设置特殊的构建标志和配置。随着扩展生态系统的不断壮大,这将迅速变成一个无法维护的混乱局面。
幸运的是,近年来 C++领域发生了很大的变化。如今,优秀的依赖管理器确实存在。其中之一是微软的 vcpkg。它已经成为 C++依赖管理器中的一个重要角色,这从