
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++依赖管理器中的一个重要角色,这从其在 GitHub 上的 20k+星星以及 CLion 和 Visual Studio 的原生支持中得到了证明。vcpkg 包含超过 2000 个依赖项,例如 Apache Arrow、yyjson 和各种云服务提供商的 SDK。
对于任何曾经使用过包管理器的人来说,使用 vcpkg 会感觉非常自然。依赖项在vcpkg.json文件中指定,vcpkg 被集成到构建系统中。现在,在构建时,vcpkg 确保在vcpkg.json中指定的依赖项被构建并可用。vcpkg 支持与多个构建系统的集成,重点是其无缝的 CMake 集成。
在 DuckDB 中使用 vcpkg
现在我们已经介绍了 DuckDB 扩展和 vcpkg,我们展示了 DuckDB 如何在不牺牲可移植性、可维护性和稳定性的情况下管理依赖。接下来,我们将通过查看 DuckDB 的一个扩展以及它如何使用 vcpkg 来管理其依赖项,使这一切更加具体。
示例:Azure 扩展
Azure 扩展提供了与微软 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使用正确的平台特定标志构建,并通过 CMake 的find_package可用。
构建自己的 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 https://github.com/⟨your-username⟩/⟨your-extension-repo⟩ --recurse-submodules
cd your-extension-repo
./scripts/bootstrap-template.py url_parser
最后,运行测试以确认一切按预期工作:
make
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
# 确认扩展工作正常
query I
SELECT url_scheme('https://github.com/duckdb/duckdb')
----
https
# 在解析错误或未找到协议时,结果为空字符串
query I
SELECT url_scheme('not:\a/valid_url')
----
(empty)
现在,我们只需要运行make test来确认一切按预期工作,然后将这些更改推送到远程仓库。随后,GitHub Actions 将接管并确保扩展为 DuckDB 支持的所有平台构建。
结论
在本博客中,我们探讨了 DuckDB 如何在其扩展生态系统中管理依赖,同时坚持其核心的“无外部依赖”理念。通过利用扩展的力量,DuckDB 能够在保持可移植性和可嵌入性的同时,提供那些需要外部依赖的基本功能。为了简化依赖管理,微软的 vcpkg 被集成到 DuckDB 的扩展构建系统中,无论是 DuckDB 维护的扩展还是第三方扩展。
如果你对创建自己的 DuckDB 扩展感兴趣,请查看以下资源:
• C++扩展模板
• DuckDB 扩展雷达:跟踪公共 DuckDB 扩展的仓库
• DuckDB 社区:加入 DuckDB 的Discord 服务器,在那里你可以获得关于扩展或任何与 DuckDB 相关的帮助。
962



被折叠的 条评论
为什么被折叠?



