Databend 基于文法(Grammar)的 SQL Fuzz 测试实战指南
2026/9/16 11:56:44 网站建设 项目流程

Databend 基于文法(Grammar)的 SQL Fuzz 测试实战指南

【免费下载链接】databendData Agent Ready Warehouse : One for Analytics, Search, AI, Python Sandbox. — rebuilt from scratch. Unified architecture on your S3.项目地址: https://gitcode.com/GitHub_Trending/da/databend

本篇技术指南以 Databend 仓库中 tests/fuzz/readme.md 为骨架,系统讲解该项目如何使用**文法生成(Grammar-based Fuzzing)**方式对 SQL 引擎进行模糊测试:从运行原理、环境准备、执行流程,到如何新增自定义文法 fuzzer 的完整四步流程。读完本文,你将掌握fuzz.pymysql_clientQueryGeneratorFuzzRunner等核心组件的实现机制,并能够独立为 Databend 编写新的 SQL 文法模糊测试。

一、Fuzz 测试的基本原理与设计目标

Databend 的 fuzz 测试位于仓库的 tests/fuzz/ 目录,其核心思路在 readme.md 中只有一句话,但非常关键:

Fuzz test get sql in given grammar, execute it using mysql client.

即:按照给定的文法(Grammar)随机生成 SQL 语句,再通过 MySQL 客户端协议把生成的 SQL 交给 Databend 执行,以此探测 SQL 引擎在非预期输入下的健壮性。

这种测试方式属于「文法驱动的黑盒模糊测试」:测试者不关心 SQL 引擎内部实现,只关心一条重要判据——凡是 Databend 自身没有返回 SQL 错误、也没有正常返回的用例,即为测试失败。它能把"语法正确但边界刁钻"的 SQL 组合(如null与比较运算符混用、多表交叉连接、group by与聚合函数叠加、order by DESC等)自动爆炸式地生成出来,是传统手写回归用例的有效补充。

该测试在 CI 中以 standalone 模式运行,相关工作流定义在 .github/actions/test_fuzz_standalone_linux/action.yml(对应 reuse.linux.yml 中test_fuzz_standalone任务)。

二、失败判定条件(failed condition)

fuzz.py中,判定一条 SQL 是否通过验证的逻辑是query_validate()函数:

def query_validate(result): if result is None: # sql success return True if "Code" in str(result): # sql error return True return False # other error as a failed test

对应 readme 中描述的失败条件:

  • 返回None:表示 SQL 执行成功,属于正常结果,通过;
  • 错误信息中包含"Code":表示这是 Databend 返回的标准 SQL 错误(Databend 的异常信息带Code: NNNN格式的错误码),属于"被引擎正常拒绝"的预期行为,同样通过;
  • 其他任何结果(例如客户端连接中断、协议异常、超时、进程崩溃等非 SQL 级错误):一律判定为测试失败

一旦某个生成的 query 触发失败,FuzzRunner.run()会立即抛出ExpansionError("query {} failed".format(query))中止整个测试,并打印出这条触发故障的 SQL,便于复现定位。这里值得注意:测试允许引擎报 SQL 错误,但不允许"意外"失败——这正是模糊测试对查询处理器健壮性的最低要求。

三、环境准备与依赖安装

按照 readme.md 的要求,运行 fuzz 测试需要:

  1. Python 3;
  2. 安装两个 Python 包:fuzzingbook(提供文法定义与校验能力)与mysql-connector(MySQL 客户端驱动)。

这两个依赖已固化在 tests/fuzz/requirements.txt 中,可直接执行:

pip3 install fuzzingbook mysql-connector # 或等价地:pip3 install -r tests/fuzz/requirements.txt

其中fuzzingbook是知名开源书籍《The Fuzzing Book》(https://www.fuzzingbook.org)配套的 Python 库,fuzz.py中引入了它的两个核心设施:

from fuzzingbook.Grammars import Grammar, is_valid_grammar
  • Grammar:字典类型的文法类型标记(键为形如<start>的非终结符,值为展开候选列表);
  • is_valid_grammar(grammar):用于校验文法是否合法(例如非终结符是否有可达的展开、是否存在无法终止的递归等)。

除了 Python 依赖,运行 fuzz 还需要一个正在监听 MySQL 协议端口(默认 3307)的 DatabendQuery 实例,这一点在下一节详述。

四、如何运行 Fuzz 测试

4.1 一键运行脚本

仓库提供了现成的 CI 脚本 scripts/ci/ci-run-fuzz-tests.sh,它会自动完成"起服务 → 跑 fuzz"全流程:

#!/bin/bash set -e echo "Starting standalone DatabendQuery and DatabendMeta" ./scripts/ci/deploy/databend-query-standalone.sh SCRIPT_PATH="$(cd "$(dirname "$0")" >/dev/null 2>&1 && pwd)" cd "$SCRIPT_PATH/../../tests/fuzz" || exit echo "Starting databend fuzz tests" python3 fuzz.py

其内部调用的 scripts/ci/deploy/databend-query-standalone.sh 会依次完成:

  1. 清理可能残留的databend-query/databend-meta进程;
  2. target/${BUILD_PROFILE}/databend-meta -c scripts/ci/deploy/config/databend-meta-node-1.toml启动 meta 节点,并通过python3 scripts/ci/wait_tcp.py --timeout 30 --port 9191等待其就绪;
  3. target/${BUILD_PROFILE}/databend-query -c scripts/ci/deploy/config/databend-query-node-1.toml --internal-enable-sandbox-tenant启动 query 节点,等待端口 8000(HTTP handler)就绪。

之后才进入tests/fuzz目录执行python3 fuzz.py

4.2 MySQL 协议连接配置

fuzz.py中的mysql_client类通过mysql.connector建立连接,默认配置如下:

config = { "user": "root", "host": "127.0.0.1", "port": 3307, "database": "default", }

其中port 3307与 CI 部署配置一致——在 scripts/ci/deploy/config/databend-query-node-1.toml 中可以看到mysql_handler_host = "0.0.0.0"mysql_handler_port = 3307。连接参数还支持通过环境变量覆盖,方便在集群或多实例环境下运行:

环境变量覆盖的配置项说明
QUERY_MYSQL_HANDLER_HOSThostDatabendQuery 的 MySQL handler 监听地址
QUERY_MYSQL_HANDLER_PORTportMySQL handler 端口
MYSQL_USERuser连接用户名
MYSQL_DATABASEdatabase默认数据库

mysql_client.run(sql)执行 SQL 时的行为与失败判定直接相关:

def run(self, sql): cursor = self._connection.cursor(buffered=True) try: cursor.execute(sql) return None # 执行成功 → 返回 None except Exception as err: print("sql: {} execute with error: {} ".format(sql, str(err))) return err # 抛异常 → 返回错误对象

五、Fuzz 的执行流程与核心组件

fuzz.py的入口逻辑非常简单:

if __name__ == "__main__": f = FuzzRunner(generator_list, QueryExecutor()) f.run()

整个执行链路由四个类协作完成:

QueryExecutor ──prepare──> 建表 + 灌入数据 │ QueryGenerator ──next()──> grammar_fuzzer 按文法随机展开出 SQL │ FuzzRunner ──run()──> 逐个执行 SQL,并用 query_validate 校验结果

5.1 数据准备:prepare_sqls

在开始 fuzz 之前,QueryExecutor.prepare()会先执行一组固定 SQL(见fuzz.py中的prepare_sqls列表),构造出 6 张结构一致的表并填充数据:

  • t1/t2/t3:普通表,schema 为(row1 INT, row2 INT NULL, row3 FLOAT, row4 BOOLEAN, row5 VARCHAR, row6 DATE, row7 TIMESTAMP, row8 ARRAY(INT))
  • random_t1/random_t2/random_t3:使用ENGINE=RANDOM的随机引擎表,schema 与上面完全一致,用于随时生成随机数据源
  • 最后通过insert into t1 select * from random_t1 limit 110;等语句,为三张普通表各灌入 110 行随机数据。

之所以精心设计这张 schema,是因为它同时覆盖了INTNULL可空列、FLOATBOOLEANVARCHARDATETIMESTAMPARRAY(INT)共 8 种有代表性的数据类型,使文法中的任意<target_rows>组合都能命中不同的类型运算路径。

5.2 文法定义:select_grammar 与 drop_grammar

fuzz.py内置了两套文法。核心的select_grammar是一个"最小实现版"的 SELECT 文法,其设计参照了 Databend 的 SELECT 语法 RFC(代码注释中标注了对应 issue #4916):

select_grammar: Grammar = { "<start>": [ "SELECT <select_list> FROM <table_reference_list> <limit_list>", "SELECT <select_list> FROM <table_reference_list> where <condition_expression> <limit_list>", "SELECT <function_reference>(<target_rows>) FROM <table_reference_list> where <condition_expression> group by <group_by_list> <limit_list>", "SELECT <function_reference>(<target_rows>) FROM <table_reference_list> group by <group_by_list> <limit_list>", "SELECT <select_list> FROM <table_reference_list> order by <expression> ASC <limit_list>", "SELECT <select_list> FROM <table_reference_list> order by <expression> DESC <limit_list>", ], "<select_list>": ["*", "<select_target>", "<select_target>,<select_target>"], "<select_target>": ["<target_rows>"], "<condition_expression>": ["<target_rows> <expr> <value>"], "<table_reference_list>": [ "<table_reference>", "<table_reference>, <table_reference>", ], "<function_reference>": ["sum", "avg", "count", "min", "max"], "<group_by_list>": ["<expression>", "<following_expression>"], "<limit_list>": ["limit 1", "limit 10", "limit 100"], "<following_expression>": ["<expression>", "<expression>,<expression>"], "<expression>": ["<target_rows>", "<target_rows>,<target_rows>"], "<table_reference>": ["t1", "t2", "t3"], "<target_rows>": ["row1", "row2", "row3", "row4", "row5", "row6", "row7", "row8"], "<expr>": [">", "<", ">=", "<=", "!=", "="], "<value>": ["1", "0", "null"], }

可以看到,<start>的 6 条展开候选覆盖了 Databend SELECT 的主要形态:基础投影、带where过滤、聚合函数 +where+group by、聚合函数 +group byorder by ASC / DESC。非终结符层层展开后,会组合出形如SELECT sum(row3) FROM t1,t2 where row1 >= null group by row2 limit 10这类"语法成立但语义刁钻"的语句——尤其是null参与比较运算、ARRAY列参与聚合、两表无连接条件的笛卡尔积等边界场景,正是模糊测试最有价值的探测面。

另一套drop_grammar则用于测试 DDL 的健壮性:

drop_grammar: Grammar = { "<start>": ["drop table <drop_option> <table_reference> <all_reference>"], "<drop_option>": ["if exists", ""], "<table_reference>": ["t1", "t2", "t3"], "<all_reference>": ["all", ""], }

两套文法在加载时都会经过合法性断言(assert is_valid_grammar(select_grammar)),从源头保证文法本身不产生非法展开。

5.3 文法展开算法:grammar_fuzzer

fuzz.py中的grammar_fuzzer()实现了经典的"自顶向下随机展开"算法(代码注释引用了《The Fuzzing Book》的 Grammars 章节):

  1. 从起始符号<start>开始;
  2. 随机挑选当前串中的一个非终结符;
  3. 从该符号的候选展开中随机选择一个替换它;
  4. 如果替换后非终结符数量超过上限max_nonterminals(默认 10),则本次替换作废并重试;连续max_expansion_trials(默认 100)次无法展开则抛出ExpansionError,避免无限递归;
  5. 直到串中不再含有非终结符,返回最终生成的 SQL 文本。

QueryGenerator对每次生成的句子还会随机化最大非终结符数量(random.randint(5, 10)),进一步增大生成 SQL 的形态多样性。

5.4 执行与调度:QueryExecutor 与 FuzzRunner

  • QueryExecutor:封装mysql_client,负责prepare()建表灌数和execute(sql)单条执行;
  • FuzzRunner:持有 generator 列表与 executor,按顺序为每个 generator 循环执行execute_times次,每次取一条生成 SQL 执行并调用query_validate()校验,一旦失败立即抛出异常终止。

fuzz.py末尾的默认调度配置为:

generator_list = [ QueryGenerator(select_grammar, 1000), # 生成 1000 条 SELECT 语句 QueryGenerator(drop_grammar, 10), # 生成 10 条 DROP 语句 ]

即默认对 SELECT 文法执行 1000 次、对 DROP 文法执行 10 次。

六、如何新增一个文法 Fuzzer(四步走)

这是 readme 给出的核心扩展指南,完整对应到fuzz.py的代码结构:

Step 1:定义新文法

fuzz.py中新增一个文法字典,例如仿照select_grammar

my_grammar: Grammar = { "<start>": ["<some_statement> <tail>"], "<some_statement>": ["...", "..."], "<tail>": ["...", ""], }

文法必须包含起始符号<start>,所有引用的非终结符都必须有对应的展开规则。

Step 2:校验文法合法性

紧跟文法定义,增加assert断言:

assert is_valid_grammar(my_grammar)

is_valid_grammar来自fuzzingbook.Grammars,会在启动阶段即时发现文法书写错误,避免带着坏文法跑完整轮测试。

Step 3:注册到 generator 列表并指定执行次数

generator_list = [ QueryGenerator(select_grammar, 1000), QueryGenerator(drop_grammar, 10), QueryGenerator(my_grammar, 100), # 新增项 ]

QueryGenerator(grammar, execute_times)的第二个参数控制该文法生成的 SQL 条数,可按测试耗时与覆盖面权衡。

Step 4:运行

python3 fuzz.py

此时FuzzRunner会自动遍历包含新文法的generator_list执行全部用例。若新文法依赖新的表或数据形态,记得同步扩充prepare_sqls,确保被测 SQL 不会因缺表而大面积报错、淹没真正有价值的失败信号。

七、与 CI 的集成方式

tests/fuzz的用例被定义为独立的 CI 任务。在 .github/actions/test_fuzz_standalone_linux/action.yml 中,该 action 会先运行bash ./scripts/setup/dev_setup.sh -yd准备构建环境,再执行bash ./scripts/ci/ci-run-fuzz-tests.sh(即 4.1 节的一键脚本),失败时上传现场工件(artifact_failure)便于排查。

需要说明的是,当前仓库的 reuse.linux.yml 中test_fuzz_standalone任务处于注释状态(对应代码位置约在 420-432 行,且带有timeout-minutes: 10continue-on-error: true),说明该测试目前作为按需/被禁用的任务保留。如果你在本地复现 CI 效果,可直接依次执行:

bash ./scripts/ci/deploy/databend-query-standalone.sh cd tests/fuzz python3 fuzz.py

前提是本地已构建好target/debug(或target/release,通过BUILD_PROFILE环境变量指定)下的databend-metadatabend-query二进制,且 fuzz 所需的 Python 依赖已按第三节安装。

八、小结

Databend 的 fuzz 测试(tests/fuzz/)是一套轻量而完整的文法驱动 SQL 模糊测试方案:以fuzzingbook的文法工具生成海量组合 SQL,通过 MySQL 协议灌入 Databend 查询引擎,并利用"非 SQL 错误的异常即失败"这一严格判据捕捉引擎的健壮性问题。其核心价值在于:

  • 覆盖手写用例难以触及的边界组合null比较、类型混用、无连接条件多表查询、聚合与group by叠加等;
  • 扩展成本极低:新增文法只需遵循"定义 → 断言 → 注册 → 运行"四步,即可批量产出新形态的测试语句;
  • 与 CI 深度集成:standalone 模式下一条命令即可完成部署与测试,失败时还会打印触发故障的 SQL 原文,便于快速复现。

对于希望为 Databend 贡献测试能力或验证本地 SQL 引擎健壮性的开发者而言,tests/fuzz/fuzz.py 是一个理想的起点:读懂它,你就能在几分钟内设计出属于自己的 SQL 文法 fuzzer。

【免费下载链接】databendData Agent Ready Warehouse : One for Analytics, Search, AI, Python Sandbox. — rebuilt from scratch. Unified architecture on your S3.项目地址: https://gitcode.com/GitHub_Trending/da/databend

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

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

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

立即咨询