GreptimeDB 兼容性测试框架(Compatibility Test Framework):从 RFC 到cargo sqlness compat的跨版本验证实践
【免费下载链接】greptimedbThe open-source observability database. One columnar engine for metrics, logs, and traces, on object storage.项目地址: https://gitcode.com/GitHub_Trending/gr/greptimedb
导读:本文围绕 GreptimeDB 的兼容性测试框架 RFC 展开,讲解如何系统化验证不同 GreptimeDB 版本之间的向后兼容与向前兼容能力。你将掌握兼容性测试用例的组织规范(feature / verify / cleanup 三段式)、
since/till/IGNORE_RESULT/TEMPLATE等 sqlness 拦截器语义、基于cargo sqlness-runner compat的实际运行方法,以及仓库中 28 个落地用例与 CI 版本窗口机制。
背景:为什么 GreptimeDB 需要一套兼容性测试框架
GreptimeDB 是一个开源的观测性数据库(observability database),以单套列式存储引擎统一承载 metrics、logs 和 traces,数据落在对象存储(object storage)之上。数据库的存储格式、元数据结构、WAL 布局会随版本持续演进,因此“旧版本写入的数据能否被新版本安全打开、新版本写入的数据能否被旧版本重新读取”就成为发布流程中的关键质量关卡。
在框架出现之前,GreptimeDB 的兼容性保障依赖手工与临时脚本(ad-hoc cases):
- 每次发版都要由 release manager 人工测试不同版本组合,耗时且容易遗漏;
- 缺少发布 SoP(Standard Operating Procedure)中关于兼容性测试的详细指南;
- 历史上曾多次在大版本发布后立刻发布的补丁版本(如
v0.14.1、v0.15.1)中暴露兼容性问题,说明人工把关并不可靠。
RFC 2025-07-04-compatibility-test-framework.md 的动机部分明确记录了这些痛点,并据此提出一套易维护、易扩展、易运行的框架:给定任意两个 GreptimeDB 版本,既能回答向后兼容(backward),也能回答向前兼容(forward)问题。
框架总体设计:测试用例 + 专用 runner
RFC 将框架分为两个组成部分:
- 测试用例(Test Cases):专为兼容性测试维护的一组用例,仍然沿用 GreptimeDB 集成测试生态中熟悉的
.sql+.result格式; - 测试框架(Test Framework):一个新的 sqlness runner,在既有 sqlness 基础设施之上增加集成测试不需要的新能力(
since/till、IGNORE_RESULT、TEMPLATE,以及自动拉取版本二进制的能力)。
该设计基于 Sqlness 库,但使用方式与普通集成测试不同:兼容性测试的核心不是校验精确的输出值,而是验证“旧版本能建的表、写的状态,新版本能否接管并正确运行”。
测试用例组织:1.feature / 2.verify / 3.cleanup 三段式
RFC 将用例集划分为三个按顺序执行的阶段,以树形目录组织:
compatibility_test/ ├── 1.feature/ │ ├── feature-a/ │ ├── feature-b/ │ └── feature-c/ ├── 2.verify/ │ ├── verify-metadata/ │ ├── verify-data/ │ └── verify-schema/ └── 3.cleanup/ ├── cleanup-a/ ├── cleanup-b/ └── cleanup-c/三个阶段各司其职:
1.feature(使用新功能):在“旧版本”上使用新版本才有的特性,制造带特定状态的数据与元数据;2.verify(验证数据库行为):在“新版本”上对旧版本产生的状态进行查询验证,确保行为正确;3.cleanup(清理环境):与1.feature配对,清理测试环境。
这三个阶段必须严格按1.feature→2.verify→3.cleanup的顺序执行。
RFC 中的示例:索引选项特性
RFC 以新增索引选项特性(对应 GreptimeTeam/greptimedb 的 PR #6416)为例,给出完整用例写法:
1.feature阶段,在旧版本上创建带新索引选项的表,使用-- SQLNESS ARG since=0.15.0标注特性自v0.15.0起可用,并用-- SQLNESS IGNORE_RESULT声明不关心执行结果、只要求执行成功:
-- path: compatibility_test/1.feature/index-option/granularity_and_false_positive_rate.sql -- SQLNESS ARG since=0.15.0 -- SQLNESS IGNORE_RESULT CREATE TABLE granularity_and_false_positive_rate (ts timestamp time index, val double) with ("index.granularity" = "8192", "index.false_positive_rate" = "0.01");3.cleanup阶段,在验证完成后删除该表:
-- path: compatibility_test/3.cleanup/index-option/granularity_and_false_positive_rate.sql drop table granularity_and_false_positive_rate;由于该特性不需要特殊的验证逻辑,2.verify阶段直接复用既有的通用验证用例。例如verify-metadata中的SHOW CREATE TABLE模板用例,结合-- SQLNESS TEMPLATE TABLE="SHOW TABLES"拦截器,可对运行时发现的每一张表执行元数据验证:
-- path: compatibility_test/2.verify/verify-metadata/show-create-table.sql -- SQLNESS TEMPLATE TABLE="SHOW TABLES"; SHOW CREATE TABLE $TABLE;维护策略:把成本压给特性实现者
RFC 提出的维护策略非常关键:每次实现一个新特性,若该特性需要被兼容性测试覆盖,实现者必须为它在1.feature/和3.cleanup/各写一个用例,并检查2.verify/中是否有可复用的现有用例。
这相当于模拟一位“热情用户”在第一时间使用全部新特性——把维护负担分摊到每个特性的实现者身上,为行为“定格”(fixation)。未来一旦出现破坏性变更,框架会自动检测到,而不需要 release manager 凭经验排查。这种设计使框架可以持续演进而不是一次性工程。
废弃特性标记:since/till版本窗口
如果某个特性被废弃,需要在用例中同步标记。RFC 以index.granularity和index.false_positive_rate两个索引选项为例,假设它们将在v0.99.0被废弃,则用例标注为:
-- SQLNESS ARG since=0.15.0 till=0.99.0 ...这告诉框架:该特性只在v0.15.0(含)到v0.99.0(不含)之间的版本参与测试。对于 GreptimeDB 中大量计划未来废弃的实验性特性,这是一种低成本、声明式的管理方式。
框架新增的 sqlness 拦截器
RFC 的第二部分定义了 runner 需要的新能力,均为 sqlness 拦截器(interceptor)层面的扩展:
SQLNESS ARG since=VERSION_STRING [till=VERSION_STRING]
沿袭 sqlness 的ARG拦截器,用注释声明一个特性在两个版本之间可用。since必填、till可选:
-- SQLNESS ARG since=VERSION_STRING [till=VERSION_STRING]runner 依据该声明决定用例在当前版本组合下是否应被跳过(例如till之后的新版本不再执行该特性用例)。
IGNORE_RESULT:只验证执行成功
IGNORE_RESULT是新增拦截器:告诉 runner 忽略查询结果,只检查查询是否成功执行。
这与集成 sqlness 测试有本质区别:兼容性测试在大多数场景下并不关心查询返回的具体值,只关心“旧版本创建的状态能否被新版本正常操作”。这个设计大幅降低了用例的维护成本——实现者无需为新特性编写并持续维护精确的结果快照。
TEMPLATE:基于运行时数据生成查询
TEMPLATE是另一个新增拦截器,可从模板结合运行时数据动态生成查询。
上面的SHOW CREATE TABLE $TABLE即典型场景:需要对新版本上现存的所有表执行SHOW CREATE TABLE,但表清单是运行时才确定的,无法静态写死在用例里。TEMPLATE拦截器(sqlness 中对应sqlness::interceptor::template,在 tests/runner/src/cmd/compat.rs 中通过TEMPLATE_DELIMITER接入)允许先用SHOW TABLES获取表清单,再逐表展开模板生成验证语句。
Runner 的额外要求与执行流程
RFC 对 runner 本身提出三点要求:
- 顺序执行:先跑
1.feature/,再跑2.verify/,最后跑3.cleanup/; - 自动拉取版本:能够自动获取所需版本的二进制完成测试;
- 正确处理
since/till:根据版本声明过滤用例。
其中1.feature阶段需要识别出所有需要测试的特性并按版本号标注;随后 runner 使用新版本(to版本)重启,再执行2.verify/和3.cleanup/阶段。这正是从源码结构看 compat.rs 中CompatCommand的实现思路:它先启动 “from” 集群执行 setup SQL,再在保留状态(preserved state)上用 “to” 版本重启集群,最后执行 verify SQL 并与verify.result对比。
RFC 设想的运行方式:./sqlness run --from=... --to=...
RFC 给出了最小化的命令行设想,例如发布v0.16.0时检查v0.15.0到v0.16.0的向后兼容:
# check backward compatibility between v0.15.0 and v0.16.0 when releasing v0.16.0 ./sqlness run --from=0.15.0 --to=0.16.0 # check forward compatibility when downgrading from v0.15.0 to v0.13.0 ./sqlness run --from=0.15.0 --to=0.13.0同时提出两个配套实践:
- 用脚本对给定版本区间内的所有版本组合跑一遍兼容性测试,快速生成全量兼容性报告;
- 仓库
Cargo.toml中的版本号始终提前 bump 到下一个大版本,使“下一个未发布版本”可作为本地测试等场景中的 “latest” 版本使用。
仓库落地:tests/compatibility与cargo sqlness-runner compat
RFC 提出后已在仓库中落地实现,核心位置包括:
- 用例目录:tests/compatibility/cases/(当前包含 28 个用例目录);
- runner 实现:tests/runner/src/cmd/compat.rs、tests/runner/src/cmd/compat_case.rs;
- 旧版 datanode 配置 overlay 处理:tests/runner/src/cmd/datanode_overlay.rs;
- CI 版本窗口配置:tests/compatibility/ci.toml;
- CI 侧驱动脚本:.github/scripts/run-compat.py 与 .github/scripts/update-compat-versions.py;
- 配套说明:tests/compatibility/README.md 与 tests/compatibility/AGENTS.md。
兼容性测试的定位是:验证一个 GreptimeDB 版本能否在另一个版本写入的状态上重启。命令入口为cargo sqlness compat,复用 sqlness-runner 基础设施。
常用命令
来自 tests/compatibility/README.md 的 Quick Start:
# 自兼容冒烟测试(仅当前二进制): cargo run -p sqlness-runner -- compat # 从某个已发布版本测试到当前版本: cargo run -p sqlness-runner -- compat --from-version v0.9.5 # 在两个本地二进制目录之间测试: cargo run -p sqlness-runner -- compat --from-bins-dir ./bins/old --to-bins-dir ./bins/new # 从当前构建降级测试到已发布二进制: cargo run -p sqlness-runner -- compat --from-bins-dir ./bins/current --to-version v1.1.4 # 以 standalone 拓扑运行单个兼容性用例: cargo run -p sqlness-runner -- compat --topology standalone --test-filter "downgrade_compatibility" # 运行指定用例: cargo run -p sqlness-runner -- compat --test-filter "basic_table" # 预览将运行的用例(不启动任何服务): cargo run -p sqlness-runner -- compat --dry-run --from-version v0.9.5 # 查看全部选项: cargo run -p sqlness-runner -- compat --help前置条件
- Docker(用于 etcd):分布式拓扑的 PR1 版本始终使用 Docker 启动 etcd 作为元数据存储;外部元数据存储是未来工作;
- from 二进制:二选一——
--from-version <version>自动拉取发布版,或--from-bins-dir <path>使用本地构建;greptime可执行文件必须直接位于给定目录下; - to 二进制:默认使用当前 debug 构建(
target/debug/greptime);可用--to-bins-dir <path>覆盖,或用--to-version <version>拉取发布版; - 自定义 target-dir:若设置了非默认
CARGO_TARGET_DIR,debug 二进制不在target/debug/greptime,应显式通过--from-bins-dir/--to-bins-dir指向自定义 target 目录;或者不使用自定义 target-dir 直接cargo build -p greptime。
用例格式:case.toml + setup.sql + verify.sql + verify.result
每个兼容性用例是 tests/compatibility/cases/ 下的一个目录,包含四个文件:
my_case/ case.toml # 元数据(必填) setup.sql # 在 from 版本上执行的 SQL(必填) verify.sql # 在 to 版本上执行的 SQL(必填) verify.result # verify.sql 的期望输出以basic_table用例为例(case.toml):
name = "basic_table" reason = "Verify basic table create/insert/alter/select compatibility across versions." introduced_by = "PR1 MVP" topologies = ["distributed"] from_range = ["*"] to_range = ["*"] features = ["table"] owner = "team"必填字段包括:name、reason、introduced_by、topologies、from_range、to_range、features、owner。可选字段namespace用于显式指定数据库命名空间(默认取目录名的清洗结果)。
其 setup.sql 在 from 版本上建表、插入、加列、再插入,构造一个带 schema 变更的历史状态:
CREATE TABLE foo(ts TIMESTAMP TIME INDEX, s STRING PRIMARY KEY, i INT); INSERT INTO foo VALUES ("2024-02-02 01:00:00+0800", "my_tag_1", 1), ("2024-02-02 02:00:00+0800", "my_tag_2", 2), ("2024-02-02 03:00:00+0800", "my_tag_3", 3); ALTER TABLE foo ADD COLUMN f FLOAT; INSERT INTO foo VALUES ("2024-02-02 04:00:00+0800", "my_tag_4", 4, 4.4), ("2024-02-02 05:00:00+0800", "my_tag_5", 5, 5.5), ("2024-02-02 06:00:00+0800", "my_tag_6", 6, 6.6);其 verify.sql 在 to 版本上查询并比对结果:
SELECT ts, i, s, f FROM foo ORDER BY ts;对应 verify.result 以 sqlness 快照风格给出期望输出:
SELECT ts, i, s, f FROM foo ORDER BY ts; +---------------------+---+----------+-----+ | ts | i | s | f | +---------------------+---+----------+-----+ | 2024-02-01T17:00:00 | 1 | my_tag_1 | | | 2024-02-01T18:00:00 | 2 | my_tag_2 | | | 2024-02-01T19:00:00 | 3 | my_tag_3 | | | 2024-02-01T20:00:00 | 4 | my_tag_4 | 4.4 | | 2024-02-01T21:00:00 | 5 | my_tag_5 | 5.5 | | 2024-02-01T22:00:00 | 6 | my_tag_6 | 6.6 | +---------------------+---+----------+-----+注意:如果verify.result缺失,runner 会根据实际输出生成该文件并判定失败——作者必须人工审查、提交生成的文件后重跑;如果实际输出与期望不一致,runner 会用实际输出更新verify.result并失败,同样需要人工核对差异(可参考 AGENTS.md 的说明)。
阶段语义
setup.sql(setup 阶段,from 版本):在 from 版本集群上执行,语句以分号结尾;普通注释用--前缀;-- SQLNESS ...拦截器注释遵循普通 sqlness 语义。setup 只需成功(任何错误都判用例失败),输出不与任何结果文件比对;verify.sql(verify 阶段,to 版本):在 to 版本集群上执行,输出与verify.result以 sqlness 快照风格比对。
从实现看,compat.rs 中维护了successful_setup_indexes等状态:无 fail-fast 时只验证 setup 成功的用例;fail-fast 时在停止前清理当前 profile。
版本区间过滤:from_range / to_range
from_range和to_range决定用例适用于哪些二进制版本组合:
| 表项 | 含义 |
|---|---|
"*" | 匹配任意版本(包括未知版本) |
"vX.Y.Z"或"=vX.Y.Z" | 精确匹配 X.Y.Z |
">=vX.Y.Z" | 匹配 X.Y.Z 及之后 |
">vX.Y.Z" | 匹配严格晚于 X.Y.Z 的版本 |
"<=vX.Y.Z" | 匹配 X.Y.Z 及之前 |
"<vX.Y.Z" | 匹配严格早于 X.Y.Z 的版本 |
区间列表按OR语义:任一条目匹配即命中。实现位于 compat_case.rs 的parse_version_constraint/version_matches_range,先解析>=、<=、==、=、>、<前缀,无操作符时视为精确匹配。
版本推断采用 best-effort 策略:
--from-version直接使用;--from-bins-dir/--to-bins-dir(或默认 debug 构建)通过运行<binary> --version推断版本(try_infer_version);- 当版本无法确定(如二进制缺失或
--version失败)时,非通配符区间会被跳过并给出提示;*通配符区间仍然匹配。
一个典型例子是legacy_jsonb用例(case.toml,对应 PR #8323):
from_range = ["<=v1.1.0"] to_range = [">=v1.1.1"]该用例仅在旧二进制 ≤ v1.1.0、新二进制 ≥ v1.1.1 时运行,用于验证旧二进制写入的 legacy JSONB 数据能被新二进制读取,且不进入 JSON2 结构化对齐路径。
旧版本 datanode 配置 overlay
部分用例需要在旧阶段给 datanode 叠加配置,通过case.toml中的严格可选表声明:
[old_config] datanode = "old-datanode.overlay.toml"规则要点:
- 只要存在
[old_config]就必须提供datanode;空表和未知键会被拒绝; - 引用路径相对于用例目录,且必须限定在该目录内;
- 叠加文件是原生 datanode TOML,runner 在启动服务或创建状态前加载并预检(preflight);
- 合并规则:仅当两侧值都是表时递归合并;标量、类型不匹配、数组、表数组则原子替换基线值;
region_engine无特殊合并行为; - runner 拥有的字段不允许被 overlay 修改:
mode、node_id、storage.data_home、meta_client_options.metasrv_addrs、wal.provider,以及 Raft WAL 的wal.dir或 Kafka WAL 的wal.broker_endpoints;runner 会将这些字段恢复为基线值(基线无值时删除),并对受保护字段的覆盖给出不显示值的告警。
命名空间隔离与批量行为
每个用例运行在自己的数据库命名空间中,避免相互干扰:
- 默认命名空间由用例目录名清洗得到(
[a-z][a-z0-9_]*); - 可用
case.toml的namespace覆盖; - 重复命名空间在发现阶段(版本过滤之前)即被拒绝;
- 每条语句前,runner 会执行一段不写入
verify.result的 prelude:通过 gRPC 执行CREATE DATABASE IF NOT EXISTS <ns>,然后对 gRPC/MySQL 语句执行USE <ns>,对 PostgreSQL 语句执行SET search_path TO '<ns>'。
批量行为方面:基线(无 overlay)profile 先运行,datanode TOML 语义等价的用例共享一个 profile,profile 串行、隔离执行;每个 profile 有独立的状态与 etcd 生命周期;用例串行执行(PR1 无并行);--dry-run只展示选中的 profiles、用例与 overlay 路径,不打印配置值、不启动服务。
拓扑与 PR1 限制
- 分布式拓扑:compat runner 启动 1 个 metasrv + 3 个 datanode + 1 个 frontend + 1 个 flownode;standalone 兼容性测试无需外部元数据存储;
- sqlness 拦截器:
-- SQLNESS ...注释按语句使用与普通 sqlness runner 相同的拦截器注册表(含 GreptimeDB 的PROTOCOL拦截器);对PROTOCOL POSTGRES,命名空间 prelude 使用SET search_path而非USE。应避免以pg_开头的未限定 PostgreSQL 协议表名——当前 PostgreSQL 兼容解析器会将其改写为pg_catalog.<table>; - 无注释式 compat 配置:compat runner 不在 SQL 注释中定义额外兼容性配置,sqlness 注释保持其普通含义。
xfail 策略(未来)
PR1 阶段所有用例预期通过;未来 PR 将加入xfail支持,要求必填issue与expiry字段。
CI 集成:滑动版本窗口与降级验证
CI 通过 tests/compatibility/ci.toml 控制一个较小的滑动版本窗口:
from_versions = ["v1.1.4", "v1.2.0"] # Recent releases that must be able to reopen tables written by the PR build. downgrade_to_versions = ["v1.1.4"]设计原则:
- PR 窗口保持“最新两个稳定 minor 线的最新 patch”,目标是尽早发现“从近期发布版升级到最新构建”的兼容问题,而不是在每个 PR 上重测全部历史版本;
- 窗口只决定采样哪些旧二进制;每个版本组合具体跑哪些用例仍由用例级
from_range/to_range决定; - 更宽的历史窗口属于 nightly 或 release-validation 工作流;
downgrade_to_versions可选列出在 PR 构建集群之后需要重启验证的发布版,这些运行只在 distributed 与 standalone 两种拓扑下选择downgrade_compatibility用例;- 用例级精确锚点(
=vX.Y.Z)不保留在 PR 窗口中:--check-anchors校验它们是已发布 tag,--nightly-window在 nightly 调度中执行它们; - GitHub Actions 工作流保持“薄壳”,把窗口加载与 compat 调用委托给 .github/scripts/run-compat.py;
- 发布 tag 落地后,运行
python .github/scripts/update-compat-versions.py --update --published-only刷新窗口。
降级兼容用例:一个端到端参考
downgrade_compatibility用例(case.toml)是“向前兼容/降级”方向的代表:
name = "downgrade_compatibility" reason = "Verify v1.1.4 can reopen tables whose region WAL options or byte-stream-split (BSS) float SSTs were written by the current binary, and can read all rows of an append-only table flushed and compacted with preserve_row_sequence enabled." introduced_by = "fix: preserve legacy region WAL options format; preserve_row_sequence" topologies = ["distributed", "standalone"] from_range = [">=v1.2.0"] to_range = ["=v1.1.4"] features = ["table", "wal", "downgrade", "append", "preserve_row_sequence", "sst", "float", "byte_stream_split"] owner = "metasrv"它验证:当前二进制(≥ v1.2.0)写入的 region WAL 选项与 byte-stream-split(BSS)浮点 SST,以及启用preserve_row_sequence后 flush/compaction 的 append-only 表,能够被 v1.1.4 重新打开并读回全部行。from_range = [">=v1.2.0"]与to_range = ["=v1.1.4"]的组合正是 RFC 中“向前兼容(downgrade)”场景的落地形态,也与ci.toml中downgrade_to_versions = ["v1.1.4"]相互印证。
与 RFC 设想的差异及演进
对比 RFC 与仓库现状,可以观察到几处演进(这些属于从代码与文档推断的合理结论,具体以仓库为准):
- 命令入口演进:RFC 设想
./sqlness run --from=... --to=...,落地为cargo run -p sqlness-runner -- compat,并增加了--from-bins-dir、--to-bins-dir、--dry-run、--test-filter、--topology等选项; - 用例粒度演进:RFC 的 “feature / verify / cleanup” 三段式演化为“目录即用例”模型(
case.toml+setup.sql+verify.sql+verify.result),1.feature与3.cleanup的对应关系由每个用例自身的 setup/verify 文件承载,并增加了case.toml元数据、版本区间过滤、命名空间隔离、datanode overlay 等机制; - 框架复用:RFC 中
since/till、IGNORE_RESULT、TEMPLATE等新能力在落地实现中通过 sqlness 拦截器注册表按语句应用(见 compat.rs 的interceptor_registry),其中TEMPLATE直接复用sqlness::interceptor::template的DELIMITER。
结语
GreptimeDB 兼容性测试框架将“版本间兼容性”从依赖 release manager 的人工排查,转变为可声明、可复用、可自动化的工程实践:特性实现者用少量 SQL 为行为“定格”,CI 用滑动版本窗口持续采样近期发布版,cargo sqlness compat在保留状态上完成跨版本重启验证。该框架覆盖了向后兼容(升级)与向前兼容(降级)两个方向,其设计——用例组织、拦截器语义、版本区间过滤、命名空间隔离——对任何需要长期维护存储格式与元数据兼容性的数据库项目都有直接参考价值。进一步阅读可查看 RFC 原文、tests/compatibility/README.md、tests/compatibility/AGENTS.md 及 runner 实现 compat.rs。
【免费下载链接】greptimedbThe open-source observability database. One columnar engine for metrics, logs, and traces, on object storage.项目地址: https://gitcode.com/GitHub_Trending/gr/greptimedb
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考