- 数据库
- 流处理
- 后端
- 数据工程
【免费下载链接】risingwave
Event streaming platform for agentic AI. Continuously ingest, transform, and serve event streams in real time, at scale.
导读:RisingWave 的
planner_test是一个基于 YAML 文件的数据驱动测试框架,用于对查询从 SQL 解析、绑定(Binder)、逻辑规划(Planner)、优化(Optimizer)到批量/流式物理计划生成的完整链路做回归验证。阅读本文后,你将掌握测试用例的编写格式、expected_outputs各类检查项的语义、输出自动更新的risedev工作流,以及如何在本地精准运行单个测试文件。
模块概览:这个测试工具解决什么问题
在 RisingWave 前端(src/frontend)中,一条 SQL 从文本到可执行计划要经过多层转换:sqlparser解析为 AST →Binder绑定为逻辑表达式 →Planner产出逻辑计划 →Optimizer做规则优化 → 再分别生成批量(batch)与流式(stream)物理计划。任何一层的行为变化都可能导致性能回退或语义错误,因此需要一个能够"固定"各阶段中间产物的测试手段。
planner_test模块正是为此而生的工具。它在 src/frontend/planner_test 目录下实现,其 README.md 明确说明:该模块是面向 binder、planner 与 optimizer 的测试工具——给定一组 SQL 作为输入,测试运行器会检查产生的逻辑算子树(logical operator tree)和物理算子树(physical operator tree)。
整个模块的文件组织如下:
- 测试运行入口:tests/planner_test_runner.rs,一个自定义 harness 的
nextest测试; - 测试数据目录:tests/testdata,输入放在
input/子目录,期望输出放在output/子目录; - 库代码:src/lib.rs 与 src/resolve_id.rs,负责解析 YAML、驱动执行与校验输出;
- 任务定义:planner_test.toml,以
cargo-make任务形式封装了do-apply-planner-test与run-planner-test两个命令。
值得注意的一个设计细节:输出目录中的 YAML 文件首行都写着"此文件由工具自动生成"(见 tests/testdata/output/basic_query.yaml 第一行),这说明整个体系是"输入是手写的,输出是机器生成的",测试维护者只负责声明要检查哪些产物,而不必手工抄写计划文本。
测试数据的组织方式:input 与 output 双目录
测试数据以 YAML 格式组织在tests/testdata下:
- 输入:
tests/testdata/input,每个文件是一组测试用例的列表,字段包括sql(必填)以及可选的id、name、before、expected_outputs等; - 输出:
tests/testdata/output,每个文件对应同名的输入文件,是运行器执行后自动生成/更新的期望结果。
从输入目录的内容(tests/testdata/input)可以看到测试面非常广:既有基础查询 basic_query.yaml、表达式与类型(expr.yaml、cast.yaml、array.yaml),也有各类连接与优化规则(join.yaml、join_ordering.yaml、predicate_pushdown.yaml、column_pruning.yaml),还有 DDL 与流式语义(create_source.yaml、sink.yaml、mv_on_mv.yaml、emit_on_window_close.yaml、match_recognize.yaml),以及 TPCH 基准查询 tpch.yaml。
测试运行器(tests/planner_test_runner.rs)会遍历tests/testdata/input下所有.yml与.yaml文件,以文件名(去掉扩展名)作为测试用例名注册到libtest-mimic框架中逐个执行,并对输出目录中"有输出但无对应输入"的孤儿文件做清理。
编写测试用例:SELECT 作为用例
在sql字段中直接写一条SELECT查询,并通过expected_outputs声明想要校验的计划类型即可。expected_outputs支持logical_plan、stream_plan、binder_error等取值。
用 binder_error 校验非法 SQL
README 给出的第一个示例用于验证绑定器对非法 SQL 的行为:
- sql: | select * from t expected_outputs: - binder_error由于表t不存在,Binder 阶段就会抛错。对照输出文件(tests/testdata/output/basic_query.yaml)可以看到期望结果是:
- sql: select * from t binder_error: | Catalog error Caused by: table or source not found: t这验证了绑定错误消息的稳定性——错误文案也是测试断言的一部分,任何影响报错信息的改动都会被测试捕获。
用 logical_plan / batch_plan 校验合法查询
若 SQL 合法,运行器会生成相应计划并与期望结果比对。README 的第二个示例:
- sql: | create table t (v1 bigint, v2 double precision); select * from t; expected_outputs: - logical_plan - batch_plancreate table语句会被当作前置环境执行(执行 DDL 而非产出计划),随后select * from t生成逻辑计划与批量计划。实际输出(tests/testdata/output/basic_query.yaml)为:
- sql: | create table t (v1 bigint, v2 double precision); select * from t; batch_plan: |- BatchExchange { order: [], dist: Single } └─BatchScan { table: t, columns: [t.v1, t.v2], distribution: SomeShard } stream_plan: |- StreamMaterialize { columns: [v1, v2, t._row_id(hidden)], stream_key: [t._row_id], pk_columns: [t._row_id], pk_conflict: NoCheck } └─StreamTableScan { table: t, columns: [t.v1, t.v2, t._row_id], stream_scan_type: SnapshotBackfill, stream_key: [t._row_id], pk: [_row_id], dist: UpstreamHashShard(t._row_id) }从计划树可以读出很多实现细节:批量计划顶层是BatchExchange(负责把多分片结果汇聚为Single分布),流式计划则是StreamMaterialize包裹StreamTableScan,扫描方式为SnapshotBackfill,隐藏列_row_id被自动加入作为流式主键。
多语句与前置 DDL
一个测试用例的sql字段可以包含多条语句(换行分隔),DDL 在前、查询在后是常见写法,运行器会顺序执行这些语句(src/lib.rs)。测试用例中同一条 SQL 内只允许一个查询语句——如果出现两条查询会直接panic!("two queries in one test case")。
更丰富的计划类型
README 仅列举了部分取值,实际TestType枚举(src/lib.rs)支持更多检查项,每个取值对应前端规划链路的一个具体阶段:
| expected_outputs 取值 | 对应的计划/结果 | 底层生成调用 |
|---|---|---|
logical_plan | 原始逻辑计划 | planner.plan(bound)后输出 |
optimized_logical_plan_for_batch | 面向批量执行优化后的逻辑计划 | .gen_optimized_logical_plan_for_batch() |
optimized_logical_plan_for_stream | 面向流式执行优化后的逻辑计划 | .gen_optimized_logical_plan_for_stream() |
batch_plan | 分布式批量物理计划 | .gen_batch_plan()+.gen_batch_distributed_plan() |
batch_plan_proto | 批量计划的 Proto JSON 表示 | batch_plan.to_batch_prost_identity(false) |
batch_local_plan | 单机本地执行的批量计划 | .gen_batch_local_plan() |
batch_distributed_plan | 分布式批量计划 | .gen_batch_distributed_plan() |
stream_plan | 创建物化视图(MV)的流式计划 | .gen_create_mv_plan() |
stream_dist_plan | 流式计划分片后的 fragment 计划 | build_graph+explain_stream_graph |
eowc_stream_plan | EOWC 语义下的流式计划 | .gen_create_mv_plan(.., EmitMode::OnWindowClose) |
eowc_stream_dist_plan | EOWC 语义下的流式分片计划 | 同上 +build_graph |
backfill_order_plan | Backfill 顺序计划(DOT 格式) | explain_backfill_order_in_dot_format |
sink_plan | Sink 计划(假设 blackhole sink) | .gen_sink_plan() |
binder_error/planner_error/optimizer_error | 各阶段报错信息 | 对应阶段捕获的错误 |
batch_error/batch_local_error | 批量计划生成阶段的错误 | 对应阶段捕获的错误 |
stream_error/eowc_stream_error | 流式计划生成阶段的错误 | 对应阶段捕获的错误 |
explain_output | EXPLAIN语句的输出 | handle_explain |
其中expected_outputs是一个集合(HashSet<TestType>),可同时声明多个,运行器只会生成被声明的那几项(src/lib.rs 中每个输出字段在生成前都会先检查expected_outputs是否包含对应TestType)。
可选的用例字段
除sql与expected_outputs外,测试用例还支持(定义见 src/lib.rs):
id:用例标识,可被其他用例的before引用;name:用例的简短描述,方便在测试报告中定位(例如 basic_query.yaml 中的test boolean expression common factor extraction);before:在执行本用例 SQL 之前先执行的一组前置用例 id,其语句会被展开执行;create_source/create_table_with_connector:通过CreateConnector结构声明以 connector 创建 source/表,需提供format、encode、name与file(文件内容或路径),底层会构造 Kafka connector 的CREATE SOURCE/TABLE语句并把 protobuf 定义写入临时文件(src/lib.rs);with_config_map:以键值对形式注入前端会话配置,运行前通过session.set_config应用。
编写测试用例:EXPLAIN 作为用例
当想要测试EXPLAIN CREATE ...或EXPLAIN (options) ...这类语句时,可以把EXPLAIN语句整体放进sql字段,此时输出字段固定为单一的explain_output:
- sql: explain select 1; expected_outputs: - explain_output运行器对EXPLAIN走的是explain::handle_explain处理器(src/lib.rs),成功时把 explain 响应文本填入explain_output,失败时填入planner_error。
真实用例见 explain.yaml:
- sql: explain (distsql, trace, verbose) select 1; expected_outputs: - explain_output - sql: | create table t1(v1 int); create table t2(v2 int); explain (logical) select * from t1 join t2 on v1=v2; expected_outputs: - explain_output - sql: | explain (logical) create table t1(v1 int); expected_outputs: - explain_output从这些用例可以看到EXPLAIN测试特别适合覆盖三类场景:
- 带选项的 EXPLAIN:
explain (logical)、explain (distsql, trace, verbose)等; - EXPLAIN DDL:
explain (logical) create table ...; - EXPLAIN 包住带 connector 的建表语句:
explain create table ... with (connector = 'kafka', ...) FORMAT PLAIN ENCODE JSON——即使该外部系统并不存在,规划阶段也能完成并输出 explain 结果。
这使EXPLAIN用例成为验证"规划器输出文本格式"与"错误路径部分输出"的高性价比手段(例如explain trace在失败时也应输出部分结果,见 explain.yaml 中带 id 的用例)。
测试执行链路:从 YAML 到计划树
测试运行器对每个用例的执行逻辑(src/lib.rs)大致如下:
- 用
Parser::parse_sql解析 SQL 为语句列表; - 对每条语句,按类型分派到不同处理路径;
- 对查询类语句(
Query、Insert、Delete、Update),构造OptimizerContext(verbose: true)并调用apply_query; apply_query(src/lib.rs)内部串联完整规划链路:- 用
Binder::new_for_batch(&session)绑定,失败则记录binder_error; - 用
Planner::new_for_stream规划出逻辑计划,失败则记录planner_error; - 按需调用
.gen_optimized_logical_plan_for_batch()/.gen_optimized_logical_plan_for_stream(),失败记录optimizer_error; - 按需生成批量计划(本地/分布式)与
batch_plan_proto,失败记录batch_error; - 按需以
EmitMode::Immediately或EmitMode::OnWindowClose生成流式计划及 fragment 图,失败记录stream_error/eowc_stream_error; - 按需生成
sink_plan(默认使用 blackhole connector、append-only 类型),失败记录sink_error;
- 用
check_result(src/lib.rs)核对"声明要检查的输出"与"实际产生的输出"是否一致,包括未声明却产生了输出、声明了却没有输出两种异常。
计划树文本由plan.explain_to_string()生成,也就是 EXPLAIN 命令展示的树状文本。
更新输出:自动应用新结果
当修改了规划器/优化器代码或新增了测试用例后,输出文件可以自动重新生成。运行:
./risedev do-apply-planner-test该任务在 planner_test.toml 中定义,实际执行的是UPDATE_EXPECT=1 cargo nextest run -p risingwave_planner_test --retries 0——通过UPDATE_EXPECT环境变量让expect-test库直接以"当前实际输出"覆盖期望文件;任务结束后还会清理输出目录中那些没有对应输入文件的孤儿输出。
README 特别提醒:alias./risedev dapt等价于do-apply-planner-test。因此在本地快速刷新所有计划快照时,也可以直接敲:
./risedev dapt运行单个测试:精确回归
运行单个或多个测试文件,使用:
./risedev run-planner-test <yaml file name> ./risedev run-planner-test tpch # Run tpch.yaml ./risedev run-planner-test # Run all tests其底层同样是cargo nextest run -p risingwave_planner_test --retries 0(planner_test.toml),文件名参数直接透传给nextest作为过滤条件。注意这里的"文件名"对应输入目录下的 YAML 文件(如tpch对应tests/testdata/input/tpch.yaml),且要求本地已安装nextest(任务依赖install-nextest)。
由于依赖nextest与UPDATE_EXPECT机制,务必在开发机本地执行这些命令,而不是在只读的仓库副本上运行;执行后生成的 output 改动需作为代码变更的一部分提交。
从源码看测试覆盖策略:以 TPCH 与优化规则为例
输入目录中大量 YAML 文件即是最直观的覆盖清单。以 TPCH 为例,tpch.yaml 中的用例通常形如:
- sql: | create table t (v1 bigint, v2 double precision); select ... from t; expected_outputs: - batch_plan - stream_plan配合输出目录中的对应文件,就形成了一组"SQL → 批量/流式计划快照"的回归基线。优化器相关目录(如 predicate_pushdown.yaml、column_pruning.yaml、join_ordering.yaml、case_when_optimization.yaml)则专门验证各类规则改写后的计划形态;从 basic_query.yaml 中也能看到大量针对布尔表达式化简、公因子提取、常量折叠的用例(如constant folding for IS TRUE, IS FALSE, IS NULL),其输出正是化简后的逻辑计划。
这套"输入声明 + 输出快照"的体系有几个显著优点:
- 改动即对比:修改优化规则或规划器后,任何计划形态变化都会在 diff 中直观暴露,开发者可以据此判断改动是否符合预期;
- 错误信息也纳入回归:
binder_error、planner_error等字段把报错文案纳入断言,避免不经意的错误信息回退; - 一键刷新 + 人工审阅:
do-apply-planner-test可批量重写输出,但重写后仍需人工检查 diff,防止"测试被自动化地错误固化"。
常见问题与注意事项
- 输出文件是自动生成的:永远不要手工编辑
tests/testdata/output下的文件,正确做法是修改input后运行./risedev do-apply-planner-test,并人工 review 生成的 diff; - 一个用例只放一个查询:
sql中多条 DDL 可以,但多条查询语句会触发运行器panic; before引用的是用例id:需要复用前置环境时,用before: [some_id]而不是重复贴 SQL;- connector 类用例必须提供
file:无论是create_source还是create_table_with_connector,源码(src/lib.rs)要求必须带file字段,否则直接panic; - 运行前提:需要
nextest(risedev任务会自动安装),且应在可写目录的开发环境中运行; - 不匹配即失败:
expected_outputs声明了但执行没产出对应结果,或没声明却产出了,都会被视为测试失败(check_result 的实现逻辑)。
延伸阅读
- 模块主文档:src/frontend/planner_test/README.md
- 测试运行器:src/frontend/planner_test/tests/planner_test_runner.rs
- 测试核心实现:src/frontend/planner_test/src/lib.rs
- 任务定义(
do-apply-planner-test/run-planner-test):src/frontend/planner_test/planner_test.toml - 输入样例:tests/testdata/input/basic_query.yaml 与 tests/testdata/input/explain.yaml
- 输出样例:tests/testdata/output/basic_query.yaml
- 数据库
- 流处理
- 后端
- 数据工程
【免费下载链接】risingwave
Event streaming platform for agentic AI. Continuously ingest, transform, and serve event streams in real time, at scale.
相关推荐
WSA安卓子系统完整安装教程:三步装好WSABuilds,内置谷歌商店与Root
WSA安卓子系统完整安装教程:三步装好WSABuilds,内置谷歌商店与Root WSABuilds 是一个免费的开源项目,让你在 Windows 10 / 1
开发工具Matter (connectedhomeip) CHIP Test Suites 详解:从 YAML 测试定义到多控制器测试生成
Matter connectedhomeip CHIP Test Suites 详解:从 YAML 测试定义到多控制器测试生成 导读 本文以 connected
物联网智能家居嵌入式通信微信QQ防撤回补丁教程:4步装好补丁,让对方撤回无效
微信QQ防撤回补丁教程:4步装好补丁,让对方撤回无效 晚上十一点四十,客户在群里发来"发票金额没错,按这个走",几秒后这条消息变成一行灰字。第二天早上你追问时,
桌面应用即时通讯
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考