# go-randgen **Repository Path**: mirrors_pingcap/go-randgen ## Basic Information - **Project Name**: go-randgen - **Description**: a QA tool to random generate sql by bnf pattern - **Primary Language**: Unknown - **License**: Not specified - **Default Branch**: master - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 0 - **Forks**: 0 - **Created**: 2020-08-19 - **Last Updated**: 2025-09-27 ## Categories & Tags **Categories**: Uncategorized **Tags**: None ## README # go randgen go 版本的 mysql randgen ## go get 安装 ``` go get -u github.com/pingcap/go-randgen/cmd/go-randgen ``` 尝试一下: ``` go-randgen -h ``` ## 编译安装 - 安装 go-bindata 命令行工具 ```bash go get -u github.com/jteeuwen/go-bindata/... ``` - 编译 go-randgen ```bash make all ``` ## 特色 1. 内置一个默认 zz 文件,也就是说你只给一个 yy 文件,就能自动生成 sql 2. 生成 sql 的过程中可以不连接数据库,非常迅速 3. 兼容 mysql randgen 的 yy 文件的语法,只要在 yy 文件中没有插入 perl 代码,就可以直接拿过来运行 4. 和 mysql randgen 支持嵌入 perl 代码类似,go randgen 支持嵌入 lua 代码 5. 纯 Go 实现,设计得非常灵活,非常易于 Hack 6. 除了 cmd 包下面的函数,其他包对外暴露的函数的实现全部是无状态,如果需要可以完全当成一个库来调用 ## Quick start ### gentest 生成测试 window functions 的 sql: ```bash # -Y 表示使用的 yy 文件 # -Q 表示生成的查询数量 # -B 表示将数据库构造语句与查询语句分开成两个文件存放 # 这里不需要指定 zz 文件是因为系统自带了一个默认的 zz 文件 ./go-randgen gentest -Y examples/windows.yy -Q 10 -B ``` 在当前目录下看到`output.data.sql`即是生成的 ddl(表结构定义) 和 dml(初始化表中数据), `output.rand.sql`即是根据 yy 文件生成的查询 sql。 上述案例使用的是系统[默认的 zz 文件 ](resource/resource/default.zz.lua),也可以自己重新写 ,然后通过`-Z`参数指定路径,具体规则见语法手册。 如果你不想生成 ddl,只想根据 yy 生成一些 sql, 可以使用`--skip-zz`跳过 ddl 的生成, 不过此时也不允许在 yy 文件中包含表名或者字段相关的关键字。 yy 文件的具体写法也见后面的语法手册 ### gendata 根据指定的 zz,往指定的 dsns 中灌入相应数据 ```bash # 在指定的 dsn 中灌入内置 zz 文件定义的数据 # 通过逗号分割多个dsn ./go-randgen gendata --dsns "root:@tcp(127.0.0.1:3306)/randgen,root:@tcp(127.0.0.1:4000)/randgen" ``` ### gensql 根据指定的 dsn,解析 yy 文件生成 sql: ```bash ./go-randgen gensql -Y examples/functions.yy \ --dsn "root:@tcp(127.0.0.1:3306)/randgen" \ -Q 100 ``` 注意`gensql`会假设 dsn 中所有表的字段及类型都是一样的(因为 randgen 生成的数据有这个特点) ### exec 指定两个 dsn,直接将生成的 sql 在两个 dsn 上执行,并 dump 出运行结果不一致的 sql 示例: ```bash ./go-randgen exec -Y examples/functions.yy \ --dsn1 "root:@tcp(127.0.0.1:4000)/randgen" \ --dsn2 "root:@tcp(127.0.0.1:3306)/randgen" \ -Q 100 ``` 分别在两个 dsn 中先通过内置的 zz 生成数据,然后通过 functions.yy 中定义的规则随机生成 100 条 sql, 在两个 dsn 中同时执行,然后对比执行结果是否一致,如果不一致, 则把相关信息输出到程序执行目录的`dump`目录下 (可以通过`--dump`选项修改 dump 目录) 如果你想让 go-randgen 一直运行下去,而不是执行有限条 sql 后停止, 可以将`-Q`设置为负数,比如`-Q -1`. 注意,默认情况下,对比两个 sql 的执行结果是无序的,比如下面两个运行结果, go randgen 会认为他们是一样的: ```sql Result1: +------+------+ | p | s | +------+------+ | 1 | aaa | | 2 | bbb | +------+------+ Result2: +------+------+ | p | s | +------+------+ | 2 | bbb | | 1 | aaa | +------+------+ ``` 如果想要精确到 byte 的有序比较的话,可以添加`--order`选项 `exec`也可以通过`--skip-zz`选项跳过数据生成的过程,此时它会采用 类似于`gensql`的方式生成 sql 并执行 ### 作为一个库 除了 cmd 目录下的包,其他所有包对外暴露的函数的实现都是无状态的,可以很安全地作为 一个库被反复调用。至于使用的方法,可以参考 cmd 包下相关命令的实现。 示例:通过 yy 的文本内容获得一个 Iterator ,并且生成 10 条 sql ```go package main import ( "fmt" "github.com/pingcap/go-randgen/grammar" "github.com/pingcap/go-randgen/grammar/sql_generator" "log" ) func main() { yy := ` { i = 1 } query: create create: CREATE TABLE {print(string.format("table%d", i)); i = i+1} (a int) ` iterator, err := grammar.NewIter(yy, "query", 5, nil, false) if err != nil { log.Fatalf("get iter err %v\n", err) } iterator.Visit(sql_generator.FixedTimesVisitor(func(_ int, sql string) { fmt.Println(sql) }, 5)) } ``` > 注意这里 NewIter 第三个参数传 nil 没有问题是因为示例的 yy 并没有使用关键字,如果 > 其中使用了 yy 的关键字,则最好使用 gendata.NewKeyfun() 来创建该参数 打印的结果为: ```sql CREATE TABLE table1 (a int) CREATE TABLE table2 (a int) CREATE TABLE table3 (a int) CREATE TABLE table4 (a int) CREATE TABLE table5 (a int) ``` ## 语法手册 ### zz 文件 #### 快速入门 zz 文件是一个 lua 脚本,zz 文件会定义三件事情: 1. 生成哪些表 2. 表中哪些字段 3. 字段中有哪些数据 以内置的 zz 文件为例: ```lua -- 表相关定义 tables = { -- 生成的表的记录数 rows = {10, 20, 30, 90}, -- 表的字符编码 charsets = {'utf8', 'latin1', 'binary'}, -- 表的分区数, 'undef' 表示不分区 partitions = {4, 6, 'undef'}, } -- 字段相关定义 fields = { -- 需要测试的数据类型 types = {'bigint', 'float', 'double', 'decimal(40, 20)', 'char(20)', 'varchar(20)'}, -- 所有的上面的数字类型都要测试带符合和不带符号两种 sign = {'signed', 'unsigned'} } -- 数据初始化相关定义 data = { -- 数字字段的生成方案 numbers = {'null', 'tinyint', 'smallint', '12.991', '1.009', '-9.183', 'decimal', }, -- 字符串字段的生成方案 strings = {'null', 'letter', 'english'}, } ``` 如上所示,在 zz 文件中必须要有三个 Table 类型的变量,分别是**tables**, **fields**和**data**. **tables**中定义表的相关属性,比如示例中的`rows`,`charsets`和`partitions` (更多的可定义属性见下面的语法手册),这些属性会被求全组合,每种组合生成一张表, 所以示例中的 tables 定义共会生成 4(rows)*3(charsets)*3(partitions)=36 张表 **fields**定义表中的字段信息,这些信息同样会被求全组合,每种组合生成一个字段, 但是上面的示例中生成的字段数目会少于 6(types)*2(sign)=12 个,因为 sign 属性只能 作用在数字类型的字段上面,对于非数字类型,go-randgen 会自动忽略该属性, 所以示例中的配置总计生成的字段数为 4(number)*2(sign)+2(char)=10 个 ,注意 randgen 生成的所有表中的字段都是一样的 **data**定义表中的数据,其中 key 代表字段类型 (具体可定义的字段类型见下面的语法手册) ,value 是一个数组,代表该类型字段的 可选值,每生成一条记录,遇到 key 类型的字段时会从 value 随机选择一个作为该条记录的值 ,可选值可以是"字面量"或者"生成器",比如上面示例中对于`numbers`的定义,`null`, `12.991`等就是字面量,会直接将其作为一个值,而像`tinyint`就是一个生成器, 如果选到它的话,它会从`-128~127`中随机选择一个值生成(具体有哪些生成器见下面的 语法手册) #### tables | 字段名称 | 含义 | 可选值 |默认值 | | -------- | ----- | ---- | ----| | rows | 表的记录数 | 任意大于 0 的数字 |[0, 1, 2, 10, 100] | | charsets | 字符编码 | 'utf8','utf8mb4','ascii','latin1','binary', 'undef' 表示不显式设置字符集|['undef'] | | partitions | 分区数 | 任意大于 0 的数字或者 'undef', 'undef' 表示不分区 |['undef'] | 可设置的字段与默认值在源码中见[gendata/tables.go](gendata/tables.go) 的`tablesVars`变量 #### fields | 字段名称 | 含义 | 可选值 |默认值 | | -------- | ----- | ---- | ----| | types | 字段类型 |任意合法的 mysql 类型|['int', 'varchar', 'date', 'time', 'datetime'] | | keys | 索引信息 |'key' 表示加索引,'undef' 表示不加|['undef', 'key']| | sign | 是否带符号|'signed', 'unsigned'|['signed']| 可设置的字段与默认值在源码中见[gendata/fields.go](gendata/fields.go) 的`fieldVars`变量 #### data data 的设置是和 mysql randgen 不太一样的地方,除了支持 numbers , blobs, temporals, enum, strings 五种梗概类型以外,还可以用 用更细的类型,比如 decimal,bigint 等等,如果存在更细类型的 key 的话, 则以更细类型的定义为准。 比如: ```lua data = { numbers = {'null', 'tinyint', 'smallint', '12.991', '1.009', '-9.183', 'decimal', }, bigint = {100, 10, 3}, } ``` 上面这个配置,在遇到 bigint 类型的字段时,每次生成数据会从 100, 10, 3 中随机选择一个,而 不会理会更粗犷的 numbers 的配置。 具体数据类型与梗概数据类型的对应关系见[gendata/data.go](gendata/data.go) 中的`summaryType`变量。 其中 'tinyint', 'smallint', 'decimal' 都是 go randgen 自带的数据生成规则。 go randgen 中支持的所有数据生成规则见 [gendata/generators/register.go](gendata/generators/register.go) 的`init`函数 ### yy 文件 #### 快速入门 一个简单的示例: ```yacc # 单行注释 /* 多行注释 */ query: select | select1 select: SELECT fields FROM _table fields: _field | _field_int ``` 他的一次生成结果可能如下: ```sql select1 SELECT 随机的一个字段 FROM 随机的一张表 SELECT 随机的一个整型字段 FROM 随机的一张表 select1 ``` #### 注释 - 单行注释`#` - 多行注释`/**/` #### 标识符的分类 - 非终结符:由小写字母,数字或者下划线组成,但是不能以数字开头 - 终结符:大写字母,特殊字符或者数字组成,但是不能以下划线开头 - 关键字:下划线开头 > 对于写在表达式右边的非终结符,如果找不到对应的产生式,也会退化成终结符 #### 关键字 关键字都是以下划线开头 获取表名和字段名的接口: - `_table`: 从生成的表中随机选择一张 - `_field`: 从生成的字段中随机选择一个 - `_field_int`: 从整型字段中随机选择一个 - `_field_char`: 从 char 和 varchar 类型字段中随机选择一个 - `_field_list`: 获取全部字段,以逗号分隔 - `_field_int_list`: 获取全部整型字段,以逗号分隔 - `_field_char_list`: 获取全部字符型字段,以逗号分隔 随机生成数据的一些糖 (字符相关的会在两边自动生成双引号): - `_digit`: 随机生成一个 0-9 的数字 - `_letter`: 随机生成一个 'a' 到 'z' 之间的字母 - `_english`: 随机生成一个英文单词 - `_int`: 随机生成一个整型 - `_date`: 生成`yyyy-MM-dd`格式的随机日期 - `_year`: 随机生成一个年份 - `_time`: 随机生成一个`hh:mm:ss`的随机时间 - `_datetime`: 随机生成一个`yyyy-MM-dd hh:mm:ss`的随机时间 没有写全,代码位于[链接中的 NewKeyfun 方法 ](/gendata/gendata.go), 可以自行查看 #### 嵌入 lua 代码 可以在大括号("{}")的包围中写 lua 代码,调用 print 可以想要的内容拼接到 sql 中 ``` query:{a = 1} CREATE TABLE {print(string.format("t%d", a))} (a INT) ``` 以上代码始终生成 sql 为`CREATE TABLE t1 (a INT)` 在代码块中可以调用 lua 标准库中的任意函数,比如: ``` # 每次随机生成 10-20 的随机数 query: {print(math.random(10,20))} ``` 正常的代码块会在每次分支被运行到的时候执行一遍。 go randgen 支持在文件的头部插入一个代码块,这个代码块在整个 sql 执行的过程中只会执行一次,称为**头部代码块**,主要用于变量或者函数的申明: ``` # 头部代码块对后面 sql 生成需要的一些变量或函数的进行申明 { i = 1 a = 100 function add(num1, num2) return num1 + num2 end } query: select select: SELECT * FROM _table WHERE where_clause where_clause: _field_int > {print(i)} | _field_char > {print(a)} | _field_int + _field_int > {print(add(i, a))} ``` 通过大括号包围 lua 代码看起来会和 lua 本身的 table 语法相矛盾, 但是你不用担心,我在解析的时候已经作了处理,可以放心大胆地 在代码块中使用 table 语法: ``` { f={a=1, b=3} arr={0,2,3,4} } query: {print(arr[f.a])} | {print(arr[f.b])} ``` 上面的代码将只会生成"0"或者"3"(注意 lua 数组的下标是从 1 开始的) 这个示例并没有什么实际意义,只是表达个意思 另一个比较重要的特性是你可以在 lua 代码块中用`_xxx()`的方式调用 yy 关键字: ``` query:{table = _table()} BEGIN ; update ; select ; END update: UPDATE {print(table)} SET _field_int = 10 select: SELECT * FROM {print(table)} ``` 上面这种写法将能够保证 `update` 和 `selet` 的是同一张随即表。 #### 常用模式 - 递归地嵌套子查询 ``` query: select select: SELECT * FROM (select) WHERE _field_int > 10 | SELECT * FROM _table WHERE _field_char = _english ``` - 可能为空的规则 ``` order: ASC |DESC | # 空规则 #....省略其他规则 ``` - 生成多条相邻的 sql 语句 有的时候我们希望相关的几条 sql 生成在相邻的位置,比如在测试 Prepared statement 时,下面例子改自[examples/functions.yy](examples/functions.yy) ``` query: SET @stmt = {print('"')} select {print('"')}; PREPARE stmt FROM @stmt_create ; EXECUTE stmt ; select: SELECT * FROM _table ``` 此时如果你指定生成的 sql 数量为 3(即`-Q`参数指定为 3)的话,那么就 会生成如下的 sql ```sql SET @stmt = " SELECT * FROM _table "; PREPARE stmt FROM @stmt_create; EXECUTE stmt; ``` 指定生成 6 条 sql 的话,就会把上面的 sql 生成两遍。 假如指定生成 sql 数目为 2 的话,那么会生成: ```sql SET @stmt = " SELECT * FROM _table "; PREPARE stmt FROM @stmt_create; ``` 从这里我们也可以看出`;`的语义,这个语义继承自 mysql randgen, 表示一次性生成数条相邻的 sql - 测试 create 语句时创建名字不冲突的表 方案一: 插入 lua 脚本,利用头部代码块 ``` # 申明 i 为 1 { i = 1 } query: create create: CREATE TABLE {print(string.format("table%d", i)); i = i+1} (a int) ``` 生成结果: ```sql CREATE TABLE table1 (a int); CREATE TABLE table2 (a int); CREATE TABLE table3 (a int); ...... ``` 方案 2:先创建表,然后再把它删除了,利用之前提到的`;`符号 ``` query: create create: CREATE TABLE t (a int); DROP TABLE t ``` 生成结果: ``` CREATE TABLE t (a int); DROP TABLE t; CREATE TABLE t (a int); DROP TABLE t; ... ``` ## How to hack 相比 mysql randgen,go randgen 最大的特点就是易于 Hack ,几乎没有任何硬编码,当你觉得缺少什么特性时,可以非常 方便地自己加上 ### hack zz data 如果你觉得 go randgen 在 zz 文件中 data 字段提供的 数据生成指令不够用时, 可以进入[gendata/generators/register.go](gendata/generators/register.go) 的`init`方法里添加。 假设你在里面添加了一个`aaa`指令,除了能够在 zz 的 data 字段中使用`"aaa"`指令外, 在 yy 文件中也会自动增加一个`_aaa`关键字可以使用 ### hack yy key word 如果觉得 yy 中提供的关键字不够用,可以在 [gendata/gendata.go](gendata/gendata.go) 中的`NewKeyfun`方法中添加。 ## 与 Mysql randgen 不同的地方 - 不要在规则尾部加分号, mysql randgen 有这个习惯,但是 go randgen 不需要这么做,我们不依赖这个分号 来判断不同的规则。当然,你加了也没什么问题,为了兼容 mysql randgen, 作了额外的处理 - 数据初始化时使用`insert`,而不是`insert ignore`,在给 unsigned 类型列 生成数据时会自动适配非负数,尝试最多 10 次随机,直到生成正数,如果十次都是负数, 则直接赋予 1 - zz 文件中 data 的定义可以使用更加精确的数据类型,而不是只有 mysql randgen 中的四种 - 生成 sql 时可以不连接数据库,利用在生成 ddl 时自动记录下的 schema,非常迅速