1. 背景与整体适配思路
1.1 为什么要让 DuckDB 通过 PostgreSQL 插件去访问 Kingbase
先交代一下背景。我在做某个数据分析项目时,数据源在 Kingbase(一款国产关系型数据库,兼容 PostgreSQL 协议)上,但分析负载很重,单机跑 SQL 经常把源库压得喘不过气。想要把数据拉出来做本地分析,又不能直接用客户端导 CSV——数据量太大、字段类型多,导来导去不仅慢,还容易出编码问题。
后来想到一条路:DuckDB 带了一个 PostgreSQL 扩展(postgresql 插件),本来是用来直接读取 PostgreSQL 数据源的。而 Kingbase 对外提供的协议与 PostgreSQL 高度兼容,理论上只要让 DuckDB 认为自己在连一个 PostgreSQL 实例,就能用现成的插件把 Kingbase 的表映射成 DuckDB 的表。
但现实没那么简单。Kingbase 的协议兼容度虽然高,却不是百分之百一致,某些版本、某些数据类型、某些元数据查询语句会跟 DuckDB 的 PostgreSQL 插件预期的不完全吻合。直接装上就跑,通常会报各种各样的错。所以需要手动修改 DuckDB 的 PostgreSQL 插件源码,重新编译,让它能适配 Kingbase。
这个活儿听起来挺硬核,其实拆开来看就是三步:拿到插件源码、用 vi 改对地方、编译出自己想要的扩展。下面把每一步的细节、坑和原理都讲清楚。
1.2 本文适合谁看
如果你符合下面任一情况,这篇文章很值得看完:
- 已经装好 DuckDB,但不知道如何让它读取 Kingbase 数据;
- 知道 DuckDB 有 postgresql 插件,但编译时被各种报错劝退;
- 在用 vi 修改插件源码时,不确定哪些文件该动、哪些字段必须改;
- 或者单纯想了解 DuckDB 扩展机制是怎么运作的,以及如何移植到其他兼容 PostgreSQL 协议的数据库上。
这篇文章不要求你有多深的 C++ 功底,但至少应该熟悉 Linux 文件操作、会用 vi 做文本编辑,并且能看懂简单的 Makefile 和 SQL 语句。如果你是第一次接触 DuckDB,建议先把它装在本地跑几个示例查询,再来折腾插件适配。
2. 环境准备与源码结构梳理
2.1 编译环境要什么
DuckDB 的 PostgreSQL 插件不是单独分发的二进制文件,而是作为 DuckDB 源码仓库里的一个扩展模块存在。所以第一步是拿到 DuckDB 完整源码,而不是只拿插件那部分。
我用的环境是这样的:
- 操作系统:CentOS 7.9 / Ubuntu 20.04 都试过,都能跑通
- 编译器:gcc 9.3 或更高版本
- CMake:3.20 以上
- Python:3.8 以上(DuckDB 构建系统依赖 Python 生成部分源码)
- Git:用于拉取源码
还有一点容易被忽略:DuckDB 编译时会根据当前系统自动检测一些库,比如 OpenSSL、zlib 等。如果你想跑完整测试,最好把这些依赖装上。如果只是编译插件,不跑全部测试,依赖可以少一些。
2.2 获取源码的正确姿势
我建议用 Git 拉取指定版本的 tag,而不是拉默认分支。因为默认分支开发活跃,改动频繁,今天能编译通过,明天可能就变了。我当时用的是 DuckDB 1.1.0 版本,对应的插件源码在extension/postgresql目录下。
git clone --branch v1.1.0 --depth 1 https://github.com/duckdb/duckdb.git cd duckdb拉下来之后,重点看这几个地方:
extension/postgresql/:插件主体源码,包括连接逻辑、类型映射、SQL 转换extension/postgresql/Makefile:插件构建入口src/:DuckDB 核心代码(一般不用动,但编译时会有依赖)scripts/:构建辅助脚本
在动手之前,我强烈建议先跑一次原生 DuckDB 编译,确保环境没问题。流程大概是:
make release如果这一步能顺利通过,说明工具链没问题,后面折腾插件时就安心多了。如果编译报错,优先解决环境问题,比如升级 CMake、安装缺失的依赖,再进行下一步。
2.3 为什么修改的根源在类型映射与 SQL 语句
vi 改插件源码,不是瞎改。你需要知道,DuckDB 的 postgresql 插件内部做了一件什么事情:
- 插件启动时,用 libpq 连接到目标数据库;
- 通过查询系统表(比如
pg_catalog.pg_attribute、pg_type等)获取表结构和类型信息; - 把 PostgreSQL 的类型映射成 DuckDB 的类型;
- 执行远程查询时,插件会把 DuckDB 的执行计划转换成 PostgreSQL SQL 语句发过去。
问题就出在第二和第三步。Kingbase 虽然兼容 PostgreSQL 协议,但系统表的名称、字段、类型编码可能跟原生 PostgreSQL 有差异。比如某些类型在 Kingbase 里的typid或者类型名跟 PostgreSQL 不一样;某些元数据查询的写法,Kingbase 解析器能接受但结果集跟插件预期不同。
所以,我们的核心工作就是两个字:适配。改掉插件里写死的 PostgreSQL 假设,让它兼容 Kingbase 的行为。
3. 用 vi 修改插件源码的核心要点
3.1 先判断要改哪些文件
打开extension/postgresql目录,主要文件有:
postgres_connection.cpp:连接相关的逻辑,比如连接参数、连接建立postgres_scan.cpp:扫描与查询发送的入口postgres_types.cpp:类型映射表,这里是修改重点postgres_utils.cpp:元数据查询和工具函数include/postgres_types.hpp:类型映射的头文件
我用 vi 打开文件后,第一件事不是急着改代码,而是用grep把可疑的写死值都找出来。比如:
grep -rn "pg_catalog" extension/postgresql/src/ grep -rn "typtype" extension/postgresql/src/这样能快速定位到所有跟 PostgreSQL 系统目录相关的代码。然后逐个判断这些查询或判断逻辑是否在 Kingbase 上也能正常工作。
3.2 类型映射怎么改
类型映射是最容易出问题的地方。原生 PostgreSQL 的类型列表很长,DuckDB 插件里有一个函数负责把 PostgreSQL 的类型 OID 转成 DuckDB 类型。Kingbase 虽然对齐了大部分类型,但有几个类型可能对不上。
举个例子:Kingbase 提供的自增序列字段类型serial,在 PostgreSQL 里本质是integer加一个默认值nextval('序列名')。DuckDB 插件在解析默认值时,会调用 PostgreSQL 的pg_get_expr函数来获取表达式文本。如果 Kingbase 对pg_get_expr的实现有偏差,拿到的表达式可能不是nextval(...)而是别的东西,导致 DuckDB 把它当作普通默认值处理,建同步表时就会报错。
处理方式是修改postgres_utils.cpp里的默认值解析函数,增加对 Kingbase 特有表达式的兼容分支。具体代码如下:
// 原代码只处理 nextval if (default_value.find("nextval(") == 0) { return "GENERATED BY DEFAULT AS IDENTITY"; } // 新增 Kingbase 兼容分支 if (default_value.find("serial_nextval(") == 0) { return "GENERATED BY DEFAULT AS IDENTITY"; }改完这一处,就能让 DuckDB 正确识别 Kingbase 的序列默认值。
另外,Kingbase 的字符串类型、数值类型在大部分情况下跟 PostgreSQL 一一对应,比如text、varchar、numeric都能直接映射。但有些类型名,比如clob(Kingbase 有,PostgreSQL 没有对应原生类型),插件里可能没有处理,导致映射失败。这个时候需要在postgres_types.cpp的类型映射表里加上:
{"clob", LogicalType::VARCHAR},3.3 连接参数里不能忽略的编码问题
Kingbase 默认的客户端编码可能跟 PostgreSQL 不太一样。DuckDB 插件连接数据库时会通过 libpq 设置client_encoding,但默认值不一定是UTF8。如果连接参数里没写死,Kingbase 可能返回GBK或者GB18030编码的数据,DuckDB 拿到之后无法正确转成 UTF-8,于是中文乱码。
在postgres_connection.cpp里,连接建立后会有一条设置编码的语句,如果没生效,你需要手动追加:
conn->SendQuery("SET client_encoding = 'UTF8'");我还遇到过更隐蔽的问题:某些 Linux 系统的 locale 不是 UTF-8,libpq根据系统 locale 设置的client_encoding也跟着变。解决方法是同时在环境变量里指定:
export LANG=en_US.UTF-8 export LC_ALL=en_US.UTF-8再修改连接参数,确保编码一致。
3.4 vi 实操细节:修改、保存与回退
既然讲了用 vi 修改,就多说几句 vi 的操作细节。
修改文件前,先做备份:
cp postgres_types.cpp postgres_types.cpp.bak然后打开文件:
vi extension/postgresql/src/postgres_types.cpp定位到特定函数,先用/搜索关键字,比如:
/LogicalType::VARCHAR按n跳转到下一个匹配,按Enter确认位置。接着按i进入插入模式,修改代码。改完了按Esc退出插入模式,输入:wq保存并退出。
如果你改了不确定的内容,想先试试看,可以用:w只保存不退出。如果想放弃修改,直接:q!退出。
很多人习惯用 VS Code 或者其他编辑器,但在服务器上跑,很多时候只有终端,没有图形界面。vi 的熟练度决定了你改代码的速度。特别是当你需要批量替换时,用 vi 的全局替换命令非常高效:
:%s/pg_catalog\.pg_namespace/kingbase_catalog.pg_namespace/g不过这个命令要慎用,别一上来就把所有的pg_catalog都替换成别的,先看清楚哪些地方是真正需要变更的。
3.5 修改后的编译验证
改完源码,回到 DuckDB 根目录,重新编译插件。
DuckDB 的构建系统比较特别,扩展模块是作为一个整体参与构建的。你需要先在extension/postgresql目录下找到一个Makefile,但实际构建还是回到根目录执行。我常用的命令是:
make release这个命令会重新编译所有代码,耗时较长。如果你只想编译 postgresql 扩展,可以试试:
make postgresql但这取决于 DuckDB 的构建系统是否支持单独扩展构建。我当时试过之后发现,最稳妥的方式还是全量构建,虽然耗时但不容易出幺蛾子。
编译输出目录在build/release/extension/postgresql/,你会看到postgresql.extension文件,这就是最终可以加载的扩展二进制。
测试加载方式是在 DuckDB CLI 里执行:
INSTALL postgresql; LOAD postgresql;如果源码启用了本地扩展,会自动加载编译出来的扩展。
4. 实操过程:从连接失败到成功读取
4.1 初次连接测试
编译好之后,启动 DuckDB,连接 Kingbase:
INSTALL postgresql; LOAD postgresql; ATTACH 'host=127.0.0.1 port=54321 user=testuser password=xxxx dbname=testdb' AS kdb (TYPE postgresql, READ_ONLY);如果前面类型映射和编码改对了,这一步通常会直接成功,然后就能查询了:
SHOW TABLES; SELECT * FROM kdb.some_table LIMIT 10;但如果运气不好,第一次会得到一堆错误。我遇到最典型的一个是:
Error: Binder Error: Unexpected type "numeric" in catalog这个报错说明,插件在扫描 Kingbase 系统表时,拿到了一个它不认识的数据类型,然后卡住了。得回到类型映射表去排查,看看numeric为什么没被正确转换。
实际上在 PostgreSQL 原生插件里,numeric类型是有映射的。但如果 Kingbase 的某个系统表返回的类型名不是numeric,而是decimal,就会触发这个错误。解决方式是加别名:
{"decimal", LogicalType::DECIMAL},4.2 表结构映射失败
还有一种情况,连接成功了,但调用SHOW TABLES时报错:
Error: Catalog Error: Table with name "xxx" does not exist我排查后发现,问题出在模式名(schema)的过滤条件上。DuckDB 插件默认只扫描public模式,Kingbase 默认也是public,但如果你建表时用了别的模式,那插件就看不到。
修改方式有两种:
- 在
ATTACH语句里指定模式名(如果插件支持的话); - 修改插件源码里的模式过滤逻辑,让它扫描所有模式。
我嫌麻烦,直接在源码里把模式过滤条件去掉了。需要改的地方在postgres_utils.cpp里查询表元数据的 SQL,原本有一段:
WHERE n.nspname = 'public'我改成了:
WHERE n.nspname NOT IN ('pg_catalog', 'information_schema')这样所有业务模式都能被扫描到,但也会把一些系统模式排除掉,避免误扫系统表。
4.3 查询执行中的类型转换错误
连接和元数据都通过了,真正的查询还会暴露新的问题。DuckDB 插件执行远程查询时,会把 DuckDB 的 SQL 转成 PostgreSQL SQL 发给 Kingbase。这一步涉及类型转换,比如 DuckDB 的TIMESTAMP转成 Kingbase 的timestamp without time zone,需要走一个转换函数。
有的类型在转换函数里没有对应分支,就会报错。比如 Kingbase 支持bytea,DuckDB 里有BLOB,但插件里没写bytea的转换分支。这个时候需要在某个Cast函数里补上:
case DuckDBType::BLOB: return "bytea";具体代码在postgres_scan.cpp的生成 SQL 部分。
4.4 性能参数调整
适配不是只保证能跑,还得保证效率。DuckDB 插件默认以cursor方式读取远程表,一次拉多少行由fetch_size决定。如果网络延迟高,默认值会拖慢整体速度。
我惯用的做法是,在postgres_connection.cpp里把 fetch size 调大,比如从默认的 1000 改成 10000:
conn->SetFetchSize(10000);这个参数并不是越大越好,因为 DuckDB 会先把数据存到本地缓存再处理,如果单次拉取太多行,内存占用会明显上升。对于单表几百万行的场景,10000 是一个比较稳妥的折中值。
另外,如果查询是只读的,一定记得在ATTACH时加上READ_ONLY,这样 DuckDB 不会试图开启写事务,连接开销会更小,查询也更稳定。
5. 常见问题与排查技巧实录
5.1 编译时报找不到头文件
拿源码直接编译,偶尔会遇到postgres.h找不到的错误。这是因为插件依赖 PostgreSQL 的开发头文件(libpq 开发包),而系统里没有装。
Ubuntu 上安装:
apt install libpq-devCentOS 上安装:
yum install postgresql-devel装好之后重新编译即可。如果还报错,检查一下环境变量CPLUS_INCLUDE_PATH是否包含 libpq 头文件路径。
5.2 连接超时或拒绝连接
Kingbase 默认端口跟 PostgreSQL 不一样(常见为 54321),连接时一定要写对。另外,Kingbase 的pg_hba.conf也要允许来自客户端的连接,否则 libpq 会报拒绝访问。
这个排查顺序很重要:
- 先测端口通不通:
telnet 127.0.0.1 54321 - 再用 psql 测试登录:
psql -h 127.0.0.1 -p 54321 -U testuser -d testdb - 如果 psql 能连但 DuckDB 连不上,检查是否驱动版本问题
5.3 数据类型映射后查询结果不对
有一个隐蔽问题:Kingbase 的bigint在 PostgreSQL 里对应int8,但某些版本中,Kingbase 返回给 libpq 的类型 OID 跟 PostgreSQL 不一样。DuckDB 插件如果按 OID 判断类型,就可能把bigint误判成text。表现就是查询不报错,但结果列类型乱了。
这个时候就要把插件里按 OID 判断类型的逻辑,改成按类型名判断。在postgres_types.cpp里,原本可能是switch (oid),我改成了if (type_name == "int8")之类的判断语句。虽然略显笨拙,但胜在稳定。
5.4 使用小技巧
在反复修改插件源码的过程中,我积累了一些实用技巧:
用 Git diff 看改动
每次改动之前,先git init并提交一个干净的版本。后续改坏了随时git diff看差异,快速定位问题。
编译时保留日志
如果想节省时间,可以把编译输出保存到文件:
make release > build.log 2>&1出错了直接tail -n 100 build.log,不用在终端翻屏。
测试用最小数据集
连接 Kingbase 后,不要一上来就全表查询。先建一张只有几行的测试表,验证基础类型映射是否正常,再逐步扩大表范围。
6. 最后分享一点实际中的体会
改完 DuckDB 的 postgresql 插件去连 Kingbase,这个事本质上不是一个通用方案,而是一次性适配工程。每次换 Kingbase 的小版本,或者改 DuckDB 版本,适配代码都可能需要重新检查。所以我把所有改动记录都放在一个文档里,写了清晰的注释,方便后续升级时对照。
有一次我升级了 DuckDB 版本,大多数改动都能直接沿用,只有类型映射处需要新增几个别名。那次踩坑之后,我更确信一点:改动越集中越好,尽量把所有 Kingbase 相关的兼容代码放在几个固定函数里,而不是散落在各个文件中。集中管理,后续维护起来才不痛苦。
另外,如果团队里有多个分析任务都在用这个插件,建议把编译好的扩展文件统一放在一个共享目录,然后给每个机器设置 DuckDB 的扩展加载路径。这样不用每台机器都编译一遍,省时省力。
如果你也在做类似的事,卡在某个报错很久,不妨先退一步,想一想“Kingbase 和 PostgreSQL 在这个环节上到底有什么差别”,往往答案就藏在差别的细节里。