☰
RisingWave Planner Test 详解:用 YAML 驱动的绑定器、规划器与优化器测试体系
2026/9/25 3:12:24 网站建设 项目流程
  • 数据库
  • 流处理
  • 后端
  • 数据工程

【免费下载链接】risingwave

Event streaming platform for agentic AI. Continuously ingest, transform, and serve event streams in real time, at scale.

项目地址:https://gitcode.com/gh_mirrors/ri/risingwave
点击查看免费下载

导读: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_plan

create 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_planEOWC 语义下的流式计划.gen_create_mv_plan(.., EmitMode::OnWindowClose)
eowc_stream_dist_planEOWC 语义下的流式分片计划同上 +build_graph
backfill_order_planBackfill 顺序计划(DOT 格式)explain_backfill_order_in_dot_format
sink_planSink 计划(假设 blackhole sink).gen_sink_plan()
binder_error/planner_error/optimizer_error各阶段报错信息对应阶段捕获的错误
batch_error/batch_local_error批量计划生成阶段的错误对应阶段捕获的错误
stream_error/eowc_stream_error流式计划生成阶段的错误对应阶段捕获的错误
explain_outputEXPLAIN语句的输出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测试特别适合覆盖三类场景:

  1. 带选项的 EXPLAIN:explain (logical)、explain (distsql, trace, verbose)等;
  2. EXPLAIN DDL:explain (logical) create table ...;
  3. EXPLAIN 包住带 connector 的建表语句:explain create table ... with (connector = 'kafka', ...) FORMAT PLAIN ENCODE JSON——即使该外部系统并不存在,规划阶段也能完成并输出 explain 结果。

这使EXPLAIN用例成为验证"规划器输出文本格式"与"错误路径部分输出"的高性价比手段(例如explain trace在失败时也应输出部分结果,见 explain.yaml 中带 id 的用例)。

测试执行链路:从 YAML 到计划树

测试运行器对每个用例的执行逻辑(src/lib.rs)大致如下:

  1. 用Parser::parse_sql解析 SQL 为语句列表;
  2. 对每条语句,按类型分派到不同处理路径;
  3. 对查询类语句(Query、Insert、Delete、Update),构造OptimizerContext(verbose: true)并调用apply_query;
  4. 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;
  5. 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.

项目地址:https://gitcode.com/gh_mirrors/ri/risingwave
点击查看免费下载

相关推荐

上一篇:如何3天搭建企业级AI交互系统?Element-Plus-X全方案解析
下一篇:轻松掌握百度网盘API:高效使用指南与实战技巧

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询