作者:白 珅
Databend 研发工程师
https://github.com/b41sh
为什么需要 SQLSmith?
在数据库系统的开发和维护过程中,测试扮演着至关重要的角色。它不仅可以验证功能的正确性,还可以发现潜在的问题,确保数据库在每个变更和迭代后保持性能和稳定性。Databend 的 CI 已经支持了多种类型的测试,主要包括:
- 单元测试(Unit Tests):用于验证代码中最小可测试单元的功能是否正常工作,主要针对函数、模块等基本功能点,确保其能够正确执行,并返回预期的结果。
- SQL 逻辑测试(SQL Logic Tests):通过 SQL 测试用例来验证 SQL 的语法和逻辑的正确性,覆盖不同的查询场景,确保查询在各种情况下都能正常运行。
- 性能测试(Performance Tests):测试新功能的性能变化,验证优化的有效性,防止性能回退。
这些测试能够保证系统在快速开发迭代过程中功能的正确性和稳定性,并覆盖常见的使用场景。但是它们也存在一些局限性,例如,测试中使用的 SQL 语句主要通过手工编写,通常较为简单,缺乏真实场景中会的出现复杂 SQL ,对各种边界条件和异常情况覆盖不足。
SQLSmith 是一个随机 SQL 查询生成器,它可以生成大量、多样化的 SQL 测试用例,从而在一定程度上模拟真实世界中的各种可能情况。相比于其它测试方法,SQLSmith 能够提高测试覆盖率,从而发现更多潜在的问题和 bug 。
SQLSmith 的实现
最早的 SQLSmith 灵感来自于 Csmith ,主要用于 PostgreSQL 的模糊测试。目前,已经有许多知名的开源数据库移植了各自版本的 SQLSmith 实现,如 CockroachDB、TiDB、RisingWave 等。
这些开源的 SQLSmith 实现使用不同的编程语言开发(包括 C++、Golang、Rust),支持的语法也各不相同,适用于各自的应用领域,我们无法直接使用它们来进行测试。为了完全支持 Databend 的语法和功能特点,我们必须用 Rust 构建自己的 SQLSmith 。
SQLSmith 主要包括三个部分,生成抽象语法树(AST)的 SQL Generator ,简化执行失败 SQL 的 SQL Reducer ,执行 SQL 并记录错误的 Runner 。
SQL Generator
SQL Generator 随机生成各种类型的抽象语法树(AST),主要包括如下几类:
- 生成多种数据类型,除了常见的基本数据类型之外,Databend 还支持了多种嵌套数据类型,包括
Array
、Map
、Tuple
等,这些类型可以多层嵌套,组合出更复杂的数据类型。 - 生成
DDL
语句,创建并修改用于测试的表,包括CREATE TABLE
,ALTER TABLE
,DELETE TABLE
等。 - 生成
DML
语句,生成随机的测试数据执行插入和修改操作,包括INSERT
,UPDATE
,DELETE
,MERGE
等。 - 生成
Query
语句,依次随机生成其内部的各个字段,包括WITH
,SelectTarget
,TableReference
,SubQuery
,ORDER ``BY
等,由于WITH
和SubQuery
包含嵌套的Query
,可以生成复杂的查询语句。 - 生成
Expression
表达式,包括Column
,Literal
,Scalar Function
,Aggregate Function
等,由于表达式的参数也可以是表达式,从而支持生成嵌套的表达式。
由于 AST 中的各个部分都会按照一定的概率随机生成,可以覆盖所有类型的 AST ,进而覆盖所有可能的 SQL 语句。通过递归调用生成器,我们还可以生成一些不常见的复杂嵌套语句,有利于发现一些隐藏的 bug 。同时,我们也需要适当控制嵌套的深度,避免生成过于复杂无法执行的 SQL。
SQL Reducer
由于 SQL Generator 生成的 SQL 语句可能会非常复杂,而造成 bug 的往往只是其中的一小部分,直接查看原始 SQL 不容易定位问题。有必要对原始 SQL 进行简化,得出能够复现该 bug 的最小 SQL,这样可以减少无关部分的干扰,方便用最简单的 SQL 复现 bug。
SQL Reducer 采用自顶向下的方法来简化 SQL,依次删除 AST 的不同组成部分,例如 WITH
,SubQuery
,Expression
等,如果删除后的 SQL 仍然可以复现出相同的 bug,我们就使用这个删除后的 SQL,否则就回退到原来的 SQL 。通过遍历 AST 的所有组成部分,最终可以得到一个最小可复现的 SQL 语句。
Runner
SQL Runner 使用 SQL Generator 按顺序不断循环生成用于测试的 SQL 语句,并连接 Databend 执行 SQL,如果执行成功则忽略,如果执行失败,说明可能存在 bug,继续按照以下流程进行处理:
- 判断是否是正常的报错,例如参数错误,语义错误等。
- 判断是否是已知的报错,例如一些未实现的功能,或已提 issue 的报错。
- 调用 SQL Reducer 进行简化生成最小可复现的 SQL 。
- 打印报错信息和简化后的 SQL 语句。
目前 Runner 已经集成到了 Databend 的 CI,在每次发布 Release 版本的时候都会执行,生成报错信息和 SQL 会被记录下来,用于进一步的分析。
SQLsmith 的效果
到目前为止,SQLSmith 已经在 Databend 运行了一个多月的时间,一共发现了 50 多个 bug,主要包括如下几类:
- 内部执行逻辑错误导致的 bug (17个)
- 函数或表达式参数校验错误导致的 bug (12个)
- 未执行正确的语义检查导致的 bug (9个)
-
unwrap
和unreachable
处理不当导致的 bug (7个) - Parser 语法解析失败的 bug (3个)
- 不同类型 cast 失败的 bug (4个)
- Parquet 数据读写错误导致的 bug (1个)
经验总结
通过分析 SQLSmith 发现的 bug,我们总结了一些常见的问题,可以帮助我们在以后的开发过程中避免出现 bug 。
-
函数的参数需要校验是否合法,并考虑各种情况,主要包括以下几类:
- 参数类型是
String
时,注意字符串为空的情况。 - 参数类型是
Int
时,注意值是一个大数的情况。 - 参数支持任意类型时,需要考虑一些不常用的类型,例如
Null
、EmptyArray
、Bitmap
等。 - 参数只支持特定类型时,需要考虑其它类型能不能自动转换,或者提前检查类型并返回错误。
- 参数类型是
-
谨慎使用
unwrap
,对于Result
和Option
类型的返回值,应该尽量显式的进行处理,返回值并不总是像预期的一样返回成功的结果。 -
开发新的功能需要了解相关 SQL 语义规则的约束条件,例如
ORDERBY
和HAVING
表达式中不允许出现Aggregate
和Window
函数。在 Binder 阶段进行符合语义规则的检查可以避免执行时发生错误。 -
重要的功能模块需要增加更多的单元测试用例,避免内部执行逻辑错误导致的 bug 。
下一步的工作
目前,SQLSmith 已经支持生成绝大部分常用的 SQL 并且能够与 CI 集成自动运行。我们计划在下一步继续完善 SQLsmith 的功能。主要包括如下几个方面:
- 增加更多的 SQL 语法支持,包括
Computed Column
、Union
等。 - 支持更多的配置项,包括表达式的嵌套深度,查询语句的复杂度等。
- 完善 SQL Reducer 的功能,生成更简化的 SQL 语句。
- 通过改进查询执行和结果分析的方法,提高 bug 发现的效率。
SQLSmith 是一个强大的工具,它可以帮助我们发现 Databend 隐藏的 bug,提高系统的稳定性和可靠性。我们期待 SQLsmith 在未来能够发挥更大的作用。
Connect With Us
Databend 是一款开源、弹性、低成本,基于对象存储也可以做实时分析的新式数仓。期待您的关注,一起探索云原生数仓解决方案,打造新一代开源 Data Cloud。
- Databend Website
- GitHub Discussions
- Slack Channel