将 pg_mooncake 与 duckdb_mooncake 迁移至 DuckDB 2.0、pg_duckdb 1.2.0 与 pgrx 0.19.3
当 Mooncake Labs 在发布 v0.2.0 后暂停对 pg_mooncake 的公开更新时,该扩展停留在早期的 DuckDB 1.3.x / pg_duckdb 快照与 pgrx 0.12 上。与此同时,PostgreSQL 社区已经演进至 PostgreSQL 18、pgrx 0.19.3 以及基于 DuckDB v2.0.0 的 pg_duckdb 1.2.0。
本文将深入介绍我们将整个 Mooncake 技术栈——pg_mooncake v0.3.0、duckdb_mooncake v0.0.2 与 moonlink v0.0.1——全面迁移至 DuckDB 2.0.0、pg_duckdb 1.2.0 与 pgrx 0.19.3 的工程改造过程。
架构回顾:三大代码仓库如何协同工作
理解这三个仓库在编译期与运行时的边界,有助于看清为什么升级 DuckDB 会同时涉及跨多个 crate 的 C++ 与 Rust 代码改造:
pg_mooncake是基于pgrx的 Rust 扩展,负责提供mooncake.create_table(dst, src)接口、拉起mooncake_worker后台进程、将逻辑复制变更流式写入内嵌的moonlink引擎,并将duckdb_mooncake编译进同一个动态库中。pg_duckdb在 PostgreSQL 进程内嵌了libduckdb(v2.0.0),负责拦截并接管涉及columnstore表的分析查询。duckdb_mooncake注册了名为mooncake的 DuckDBStorageExtension(存储扩展),通过 Moonlink 的 UNIX 域套接字(mooncake.sock)解析列存表元数据,等待目标 PostgreSQL WAL LSN 同步完成,并将底层文件扫描委托给 DuckDB 的并行parquet_scan读取器以及内存态 Puffin 删除向量读取器。
1. 将 duckdb_mooncake 适配至 DuckDB 2.0.0 C++ 接口
DuckDB 2.0.0(d54cbbe3df17e8e5ce68312f483b4155836112aa)在目录管理(Catalog)、存储扩展(StorageExtension)以及谓词下推(TableFilterSet)层引入了多项不兼容的 C++ API 变更:
StorageExtension 注册方式(StorageExtension::Register)
在 DuckDB 1.x 中,扩展直接修改 config.storage_extensions["mooncake"] 字典;而在 DuckDB 2.0 中,存储扩展统一通过 StorageExtension::Register(config, "mooncake", make_shared_ptr<MooncakeStorageExtension>()) 注册,且 StorageExtensionInfo 结构体的虚表与回调签名也随之更新。
TableCatalogEntry::GetColumns() 与 GetTypes()
MooncakeTableEntry 继承自 TableCatalogEntry。我们将其构造函数与列投影辅助方法对齐至 DuckDB 2.0 的 ColumnList 与 LogicalType 接口,同时保留了供 Moonlink 删除向量使用的虚拟伪列 __row_id(UBIGINT)。
TableFilterSet 迭代器重构与 IS NULL 谓词下推修复
在 DuckDB 1.x 中,TableFilterSet 直接暴露了 std::unordered_map<idx_t, unique_ptr<TableFilter>> filters 成员变量;而在 DuckDB 2.0 中,TableFilterSet 封装了内部映射,改为提供返回 (FilterIndex, const TableFilter &) 的迭代器接口。
在将 MooncakeScanFunction::mooncake_scan_bind 迁移至新迭代器接口的过程中,我们定位并修复了一个隐藏在 IS NULL / IS NOT NULL 谓词下推中的列索引重映射缺陷:
- 当查询只投影部分列(例如
SELECT b FROM t_col WHERE a IS NOT NULL AND b > 10)时,CONJUNCTION_AND与比较谓词都会将列索引从扫描投影下标重映射为底层 Parquet 物理表列下标(column_ids[filter_col_idx]),但原先的IS_NULL与IS_NOT_NULL分支却直接将未映射的投影下标传给了底层 Parquet 过滤器集合。 - 在 DuckDB 2.0 下,当执行
SELECT count(*) FROM (SELECT DISTINCT c, a, b FROM t WHERE c IS NULL AND b > 2)时,错位的列索引会导致parquet_scan触发段错误(SIGSEGV)。 - 通过
column_ids统一重映射IS_NULL与IS_NOT_NULL的列索引后,该崩溃问题彻底消除。
2. 跨 pg_mooncake 与 duckdb_mooncake 打通 LSN 一致性快照读取
由于 pg_mooncake 通过 PostgreSQL 逻辑复制异步消费行存表写入,紧跟在 INSERT 或 COMMIT 之后执行的 SELECT 必须等待 moonlink 消费完当前已刷盘的 WAL 位置,才能保证“读己之所写(Read-Your-Own-Writes)”。
在 v0.3.0 中,我们打通了跨动态库的 LSN 桥接机制:
- 在
pg_mooncake(src/lib.rs)中导出 C 符号: - 在
duckdb_mooncake中,存储扩展在运行时通过dlsym(RTLD_DEFAULT, "mooncake_get_postgres_lsn")解析该符号(若duckdb_mooncake运行在 PostgreSQL 进程之外的独立 DuckDB CLI 中,则自动回退至0)。 - 当绑定
columnstore表扫描时,duckdb_mooncake将获取到的 LSN 传入moonlink_scan_table_begin(...),由 Moonlink 阻塞等待直至逻辑复制消费进度追上目标 LSN。
3. 分区表层级自动设置 REPLICA IDENTITY FULL
PostgreSQL 逻辑复制要求无主键表(或需要获取更新/删除前完整旧元组的表)启用 REPLICA IDENTITY FULL。当用户对分区表执行 CALL mooncake.create_table('sales_col', 'sales') 时,仅在父分区表上设置 REPLICA IDENTITY FULL 并不会自动向下传播到已有的叶子分区。
pg_mooncake 0.3.0 在 mooncake.create_table 中调用 pg_sys::find_all_inheritors(relid, pg_sys::AccessExclusiveLock as i32, std::ptr::null_mut()),在单次原子操作中为父表及所有现有子分区统一设置 REPLICA IDENTITY FULL。
4. 端到端实机验证
完成适配的技术栈已在两套纯净的 Linux ARM64 环境中通过完整验证:
- Ubuntu 24.04.5 LTS ARM64(
mooncake-u24a-pg18,PostgreSQL18.6) - Rocky Linux 10.2 ARM64(
mooncake-el10a-pg17,PostgreSQL17.11)
全部 58 项 Moonlink table_handler 单元测试、3 组 SQL 回归测试集(small、partition、unsupported)、pg_duckdb 1.2.0 运行时版本校验门禁以及 13 项数据库重启持久化与 Iceberg 元数据检查点均零失败通过。