DuckDB 扩展中的依赖管理
要点:虽然核心 DuckDB 没有任何外部依赖项,但现在构建具有依赖项的扩展非常简单,它内置了对 vcpkg 的支持,vcpkg 是一个开源包管理器,支持 2000 多个 C/C++ 包。有兴趣构建自己的扩展吗?请查看扩展模板。
介绍
自从 DuckDB 诞生以来,它的主要支柱之一就是其严格的无外部依赖项理念。引用这篇 2019 年关于 DuckDB 的 SIGMOD 论文:为了满足“可嵌入性”和可移植性的实际要求,数据库需要在主机所在的任何环境中运行。已经发现,编译时或运行时对外部库(例如,openssh)的依赖是有问题的。
在这篇博文中,我们将介绍 DuckDB 如何在不强迫 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++ 格局发生了很大变化。如今,确实存在好的依赖管理器。其中之一是 Microsoft 的 vcpkg。它已成为 C++ 依赖管理器中一个非常著名的参与者,其 20k+ GitHub 星星以及 CLion 和 Visual Studio 的原生支持证明了这一点。vcpkg 包含 2000 多个依赖项,例如 Apache Arrow、yyjson 和 各种 云 提供商 SDK。
对于任何曾经使用过包管理器的人来说,使用 vcpkg 都会感觉非常自然。依赖项在 vcpkg.json
文件中指定,并且 vcpkg 已连接到构建系统。现在,在构建时,vcpkg 确保构建并提供 vcpkg.json
中指定的依赖项。vcpkg 支持与多个构建系统集成,重点是其无缝的 CMake 集成。
将 vcpkg 与 DuckDB 一起使用
现在我们介绍了 DuckDB 扩展和 vcpkg,我们展示了 DuckDB 如何在不过多牺牲可移植性、可维护性和稳定性的情况下管理依赖项。接下来,我们将通过查看 DuckDB 的一个扩展以及它如何使用 vcpkg 来管理其依赖项,使事情变得更加具体。
示例:Azure 扩展
Azure 扩展提供了与 Microsoft Azure(主要云提供商之一)相关的功能。DuckDB 的 Azure 扩展依赖于 Azure C++ SDK 来支持直接从 Azure 存储读取数据。为此,它添加了一个自定义文件系统和密钥类型,可用于轻松地从经过身份验证的 Azure 容器进行查询
CREATE SECRET az1 (
TYPE azure,
CONNECTION_STRING 'redacted'
);
SELECT column_a, column_b
FROM 'az://my-container/some-file.parquet';
为了实现这些功能,Azure 扩展依赖于 Azure SDK 的不同部分。这些在 Azure 扩展的 vcpkg.json
中指定
{
"dependencies": [
"azure-identity-cpp",
"azure-storage-blobs-cpp",
"azure-storage-files-datalake-cpp"
]
}
然后,在 Azure 扩展的 CMakelists.txt
文件中,我们找到以下几行
find_package(azure-identity-cpp CONFIG)
find_package(azure-storage-blobs-cpp CONFIG)
find_package(azure-storage-files-datalake-cpp CONFIG)
target_link_libraries(${EXTENSION_NAME} Azure::azure-identity Azure::azure-storage-blobs Azure::azure-storage-files-datalake)
target_include_directories(${EXTENSION_NAME} PRIVATE Azure::azure-identity Azure::azure-storage-blobs Azure::azure-storage-files-datalake)
基本上就是这样!每次构建 Azure 扩展时,都会首先调用 vcpkg,以确保使用正确的平台特定标志构建 azure-identity-cpp
、azure-storage-blobs-cpp
和 azure-storage-files-datalake-cpp
,并通过 find_package
在 CMake 中提供它们。
构建自己的 DuckDB 扩展
到目前为止,我们一直专注于从核心 DuckDB 贡献者的角度管理依赖项。但是,所有这些都适用于想要构建扩展的任何人。DuckDB 维护一个 C++ 扩展模板,其中包含所有必要的构建脚本、CI/CD 管道和 vcpkg 配置,以便在几分钟内构建、测试和部署 DuckDB 扩展。它可以自动为所有可用平台构建可加载的扩展二进制文件,包括 Wasm。
设置扩展模板
为了演示此过程有多么简单,让我们完成从头开始构建 DuckDB 扩展的所有步骤,包括添加 vcpkg 管理的外部依赖项。
首先,您需要安装 vcpkg
git clone https://github.com/Microsoft/vcpkg.git
./vcpkg/bootstrap-vcpkg.sh
export VCPKG_TOOLCHAIN_PATH=`pwd`/vcpkg/scripts/buildsystems/vcpkg.cmake
然后,您可以通过单击“使用此模板”来创建一个基于 该模板的 GitHub 存储库。
现在克隆您新创建的扩展存储库(包括其子模块)并初始化模板
git clone --recurse-submodules \
https://github.com/⟨your_username⟩/⟨your_extension_repo⟩
cd your-extension-repo
./scripts/bootstrap-template.py url_parser
最后,为了确认一切正常,请运行测试
make test
添加功能
当然,目前扩展有点无聊。因此,让我们添加一些功能!为了简单起见,我们将添加一个标量函数,该函数解析 URL 并返回方案。我们将该函数称为 url_scheme
。我们首先在 vcpkg.json
文件中添加对 boost url 库的依赖项
{
"dependencies": [
"boost-url"
]
}
然后,我们继续更改我们的 CMakelists.txt
,以确保我们的依赖项正确包含在构建中。
find_package(Boost REQUIRED COMPONENTS url)
target_link_libraries(${EXTENSION_NAME} Boost::url)
target_link_libraries(${LOADABLE_EXTENSION_NAME} Boost::url)
然后,在 src/url_parser_extension.cpp
中,我们删除默认示例函数,并将其替换为我们对 url_scheme
函数的实现
inline void UrlParserScalarFun(DataChunk &args, ExpressionState &state, Vector &result) {
auto &name_vector = args.data[0];
UnaryExecutor::Execute<string_t, string_t>(
name_vector, result, args.size(),
[&](string_t url) {
string url_string = url.GetString();
boost::system::result<boost::urls::url_view> parse_result = boost::urls::parse_uri( url_string );
if (parse_result.has_error() || !parse_result.value().has_scheme()) {
return string_t();
}
string scheme = parse_result.value().scheme();
return StringVector::AddString(result, scheme);
});
}
static void LoadInternal(DatabaseInstance &instance) {
auto url_parser_scalar_function = ScalarFunction("url_scheme", {LogicalType::VARCHAR}, LogicalType::VARCHAR, UrlParserScalarFun);
ExtensionUtil::RegisterFunction(instance, url_parser_scalar_function);
}
编写完扩展后,我们可以运行 make
来构建 DuckDB 和扩展。构建完成后,我们就可以尝试我们的扩展了。由于构建过程还会构建一个全新的 DuckDB 二进制文件,并自动加载扩展,因此我们只需运行 ./build/release/duckdb
,就可以使用我们新添加的标量函数了
SELECT url_scheme('https://github.com/duckdb/duckdb');
最后,由于我们是行为良好的开发人员,因此我们通过覆盖默认测试 test/sql/url_parser.test
来添加一些测试,内容如下
require url_parser
# Confirm the extension works
query I
SELECT url_scheme('https://github.com/duckdb/duckdb')
----
https
# On parser errors or not finding a scheme, the result is also an empty string
query I
SELECT url_scheme('not:\a/valid_url')
----
(empty)
现在剩下的就是使用 make test
确认一切正常,并将这些更改推送到远程存储库。然后,GitHub Actions 将接管并确保为 DuckDB 支持的所有平台构建扩展。
有关更多详细信息,请查看模板存储库。此外,我们在这篇博文中构建的示例扩展已发布在 GitHub 上。请注意,在演示中,Wasm 和 MinGW 构建由于 boost-url 依赖项在这些平台上构建而导致的悬而未决的问题已被禁用。随着这些问题在上游得到修复,重新启用扩展的构建非常简单。当然,作为此扩展的作者,在 vcpkg 中自己修复这些编译问题,不仅为该扩展,而且为整个开源社区修复它们,可能非常有意义!
结论
在这篇博文中,我们探讨了 DuckDB 在其扩展生态系统中管理依赖项的历程,同时坚持其零外部依赖项的核心理念。通过利用扩展的强大功能,DuckDB 可以在保持其可移植性和可嵌入性的同时,仍然提供需要外部依赖项的基本功能。为了简化依赖项的管理,Microsoft 的 vcpkg 集成到 DuckDB 的扩展构建系统中,既适用于 DuckDB 维护的扩展,也适用于第三方扩展。
如果这篇博文激发了您创建自己的 DuckDB 扩展的兴趣,请查看 C++ 扩展模板、DuckDB 关于扩展的文档以及非常有用的 duckdb-extension-radar 存储库,该存储库跟踪公共 DuckDB 扩展。此外,DuckDB 还有一个 Discord 服务器,您可以在其中寻求有关扩展或任何与 DuckDB 相关问题的帮助。