diff --git a/Makefile b/Makefile index 782b414bb35d32a9b4d24f897438dee67afd30a6..fd95aaa94d3c2ee98daff9ceb9fe8ee3ecd14996 100644 --- a/Makefile +++ b/Makefile @@ -17,9 +17,9 @@ VERSION_TAG := $(shell git describe --tags --always) VERSION_VERSION := $(shell git log --date=iso --pretty=format:"%cd" -1) $(VERSION_TAG) VERSION_COMPILE := $(shell date +"%F %T %z") by $(shell go version) VERSION_BRANCH := $(shell git rev-parse --abbrev-ref HEAD) -VERSION_GIT_DIRTY := $(shell git diff --no-ext-diff 2>/dev/null | wc -l | awk '{print 1}') +VERSION_GIT_DIRTY := $(shell git diff --no-ext-diff 2>/dev/null | wc -l | awk '{print $1}') VERSION_DEV_PATH:= $(shell pwd) -LDFLAGS=-ldflags="-s -w -X 'github.com/XiaoMi/soar/common.Version=$(VERSION_VERSION)' -X 'github.com/XiaoMi/soar/common.Compile=$(VERSION_COMPILE)' -X 'github.com/XiaoMi/soar/common.Branch=$(VERSION_BRANCH)' -X github.com/XiaoMi/soar/common.GitDirty=$(VERSION_GIT_DIRTY) -X github.com/XiaoMi/soar/common.DevPath=$(VERSION_DEV_PATH)" +LDFLAGS=-ldflags="-s -w -X 'github.com/XiaoMi/soar/common.Version=$(VERSION_VERSION)' -X 'github.com/XiaoMi/soar/common.Compile=$(VERSION_COMPILE)' -X 'github.com/XiaoMi/soar/common.Branch=$(VERSION_BRANCH)' -X 'github.com/XiaoMi/soar/common.GitDirty=$(VERSION_GIT_DIRTY)' -X 'github.com/XiaoMi/soar/common.DevPath=$(VERSION_DEV_PATH)'" # These are the values we want to pass for VERSION and BUILD BUILD_TIME=`date +%Y%m%d%H%M` diff --git a/advisor/heuristic.go b/advisor/heuristic.go index 813e1ded61e4f89ff7f441b8c79725422a47a434..511e556d2367857bb09c6aeb2a770a061cc3b5f7 100644 --- a/advisor/heuristic.go +++ b/advisor/heuristic.go @@ -279,7 +279,7 @@ func (idxAdv *IndexAdvisor) RuleImplicitConversion() Rule { continue } - // 检查排序排序不一致导致的隐式数据转换 + // 检查 collation 排序不一致导致的隐式数据转换 common.Log.Debug("Collation: `%s`.`%s` (%s) VS `%s`.`%s` (%s)", colList[0].Table, colList[0].Name, colList[0].Collation, colList[1].Table, colList[1].Name, colList[1].Collation) @@ -322,6 +322,7 @@ func (idxAdv *IndexAdvisor) RuleImplicitConversion() Rule { isCovered := true if tps, ok := typMap[val.Type]; ok { for _, tp := range tps { + // colList[0].DataType, eg. year(4) if strings.HasPrefix(colList[0].DataType, tp) { isCovered = false } @@ -339,6 +340,18 @@ func (idxAdv *IndexAdvisor) RuleImplicitConversion() Rule { common.Log.Debug("Implicit data type conversion: %s", c) content = append(content, c) + } else { + // 检查时间格式,如:"", "2020-0a-01" + switch strings.Split(colList[0].DataType, "(")[0] { + case "date", "time", "datetime", "timestamp", "year": + if !timeFormatCheck(string(val.Val)) { + c := fmt.Sprintf("%s 表中列 %s 的时间格式错误,%s。", colList[0].Table, colList[0].Name, string(val.Val)) + common.Log.Debug("Implicit data type conversion: %s", c) + content = append(content, c) + } + // TODO: 各种数据类型格式检查 + default: + } } } @@ -355,6 +368,15 @@ func (idxAdv *IndexAdvisor) RuleImplicitConversion() Rule { return rule } +// timeFormatCheck 时间格式检查,格式正确返回 true,格式错误返回 false +func timeFormatCheck(t string) bool { + // 不允许为空,但允许时间前后有空格 + t = strings.TrimSpace(t) + // 仅允许 数字、减号、冒号、空格 + allowChars := regexp.MustCompile(`^[\-0-9: ]+$`) + return allowChars.MatchString(t) +} + // RuleNoWhere CLA.001 & CLA.014 & CLA.015 func (q *Query4Audit) RuleNoWhere() Rule { var rule = q.RuleOK() @@ -832,7 +854,7 @@ func (q *Query4Audit) RuleAddDefaultValue() Rule { } switch c.Tp.Tp { - case mysql.TypeBlob, mysql.TypeTinyBlob, mysql.TypeMediumBlob, mysql.TypeLongBlob: + case mysql.TypeBlob, mysql.TypeTinyBlob, mysql.TypeMediumBlob, mysql.TypeLongBlob, mysql.TypeJSON: colDefault = true } @@ -855,7 +877,7 @@ func (q *Query4Audit) RuleAddDefaultValue() Rule { } switch c.Tp.Tp { - case mysql.TypeBlob, mysql.TypeTinyBlob, mysql.TypeMediumBlob, mysql.TypeLongBlob: + case mysql.TypeBlob, mysql.TypeTinyBlob, mysql.TypeMediumBlob, mysql.TypeLongBlob, mysql.TypeJSON: colDefault = true } @@ -1253,20 +1275,66 @@ func (q *Query4Audit) RuleImpossibleWhere() Rule { // RuleMeaninglessWhere RES.007 func (q *Query4Audit) RuleMeaninglessWhere() Rule { var rule = q.RuleOK() - // SELECT * FROM tb WHERE 1 + + var where *sqlparser.Where switch n := q.Stmt.(type) { case *sqlparser.Select: - if n.Where != nil { - switch n.Where.Expr.(type) { - case *sqlparser.SQLVal: + where = n.Where + case *sqlparser.Update: + where = n.Where + case *sqlparser.Delete: + where = n.Where + } + if where != nil { + switch v := where.Expr.(type) { + // WHERE 1 + case *sqlparser.SQLVal: + switch string(v.Val) { + case "0", "false": + default: + rule = HeuristicRules["RES.007"] + return rule + } + // WHERE true + case sqlparser.BoolVal: + if v { rule = HeuristicRules["RES.007"] return rule } } } - // 1=1, 0=0 + err := sqlparser.Walk(func(node sqlparser.SQLNode) (kontinue bool, err error) { switch n := node.(type) { + // WHERE id = 1 or 2 + case *sqlparser.OrExpr: + // right always true + switch v := n.Right.(type) { + case *sqlparser.SQLVal: + switch string(v.Val) { + case "0", "false": + default: + rule = HeuristicRules["RES.007"] + } + case sqlparser.BoolVal: + if v { + rule = HeuristicRules["RES.007"] + } + } + // left always true + switch v := n.Left.(type) { + case *sqlparser.SQLVal: + switch string(v.Val) { + case "0", "false": + default: + rule = HeuristicRules["RES.007"] + } + case sqlparser.BoolVal: + if v { + rule = HeuristicRules["RES.007"] + } + } + // 1=1, 0=0 case *sqlparser.ComparisonExpr: factor := false switch n.Operator { @@ -1300,6 +1368,12 @@ func (q *Query4Audit) RuleMeaninglessWhere() Rule { if (bytes.Equal(left, right) && !factor) || (!bytes.Equal(left, right) && factor) { rule = HeuristicRules["RES.007"] } + + // TODO: + // 2 > 1 + // true = 1 + // false != 1 + return false, nil } return true, nil @@ -2681,8 +2755,14 @@ func (q *Query4Audit) RuleAlterCharset() Rule { for _, option := range spec.Options { if option.Tp == tidb.TableOptionCharset || option.Tp == tidb.TableOptionCollate { - rule = HeuristicRules["ALT.001"] - break + //增加CONVERT TO的判断 + convertReg, _ := regexp.Compile("convert to") + if convertReg.Match([]byte(strings.ToLower(q.Query))) { + break + } else { + rule = HeuristicRules["ALT.001"] + break + } } } } @@ -2759,7 +2839,7 @@ func (q *Query4Audit) RuleBLOBNotNull() Rule { continue } switch col.Tp.Tp { - case mysql.TypeBlob, mysql.TypeTinyBlob, mysql.TypeMediumBlob, mysql.TypeLongBlob: + case mysql.TypeBlob, mysql.TypeTinyBlob, mysql.TypeMediumBlob, mysql.TypeLongBlob, mysql.TypeJSON: for _, opt := range col.Options { if opt.Tp == tidb.ColumnOptionNotNull { rule = HeuristicRules["COL.012"] @@ -2782,7 +2862,7 @@ func (q *Query4Audit) RuleBLOBNotNull() Rule { continue } switch col.Tp.Tp { - case mysql.TypeBlob, mysql.TypeTinyBlob, mysql.TypeMediumBlob, mysql.TypeLongBlob: + case mysql.TypeBlob, mysql.TypeTinyBlob, mysql.TypeMediumBlob, mysql.TypeLongBlob, mysql.TypeJSON: for _, opt := range col.Options { if opt.Tp == tidb.ColumnOptionNotNull { rule = HeuristicRules["COL.012"] @@ -3109,7 +3189,8 @@ func (q *Query4Audit) RuleColumnWithCharset() Rule { for _, tk := range tks { if tk.Type == ast.TokenTypeWord { switch strings.TrimSpace(strings.ToLower(tk.Val)) { - case "national", "nvarchar", "nchar", "nvarchar(", "nchar(", "character": + //character移到后面检查 + case "national", "nvarchar", "nchar", "nvarchar(", "nchar(": rule = HeuristicRules["COL.014"] return rule } @@ -3125,6 +3206,16 @@ func (q *Query4Audit) RuleColumnWithCharset() Rule { continue } if col.Tp.Charset != "" || col.Tp.Collate != "" { + if col.Tp.Charset == "binary" || col.Tp.Collate == "binary" { + continue + } else { + rule = HeuristicRules["COL.014"] + break + } + } + //在这里检查character + characterReg, _ := regexp.Compile("character set") + if characterReg.Match([]byte(strings.ToLower(q.Query))) { rule = HeuristicRules["COL.014"] break } @@ -3139,6 +3230,15 @@ func (q *Query4Audit) RuleColumnWithCharset() Rule { continue } if col.Tp.Charset != "" || col.Tp.Collate != "" { + if col.Tp.Charset == "binary" || col.Tp.Collate == "binary" { + continue + } else { + rule = HeuristicRules["COL.014"] + break + } + } + characterReg, _ := regexp.Compile("character set") + if characterReg.Match([]byte(strings.ToLower(q.Query))) { rule = HeuristicRules["COL.014"] break } @@ -3343,7 +3443,7 @@ func (q *Query4Audit) RuleBlobDefaultValue() Rule { continue } switch col.Tp.Tp { - case mysql.TypeBlob, mysql.TypeMediumBlob, mysql.TypeTinyBlob, mysql.TypeLongBlob: + case mysql.TypeBlob, mysql.TypeMediumBlob, mysql.TypeTinyBlob, mysql.TypeLongBlob, mysql.TypeJSON: for _, opt := range col.Options { if opt.Tp == tidb.ColumnOptionDefaultValue && opt.Expr.GetType().Tp != mysql.TypeNull { rule = HeuristicRules["COL.015"] @@ -3362,7 +3462,7 @@ func (q *Query4Audit) RuleBlobDefaultValue() Rule { continue } switch col.Tp.Tp { - case mysql.TypeBlob, mysql.TypeMediumBlob, mysql.TypeTinyBlob, mysql.TypeLongBlob: + case mysql.TypeBlob, mysql.TypeMediumBlob, mysql.TypeTinyBlob, mysql.TypeLongBlob, mysql.TypeJSON: for _, opt := range col.Options { if opt.Tp == tidb.ColumnOptionDefaultValue && opt.Expr.GetType().Tp != mysql.TypeNull { rule = HeuristicRules["COL.015"] diff --git a/advisor/heuristic_test.go b/advisor/heuristic_test.go index a5a0a8ef95fc633042c89e9a2525c1afb5930e0d..b7402dcdad437562f911f2f365c64c1f52ccb988 100644 --- a/advisor/heuristic_test.go +++ b/advisor/heuristic_test.go @@ -150,6 +150,31 @@ func TestRuleEqualLike(t *testing.T) { common.Log.Debug("Exiting function: %s", common.GetFunctionName()) } +// ARG.003 +// TODO: + +func TestTimeFormatError(t *testing.T) { + rightTimes := []string{ + `2020-01-01`, + } + for _, rt := range rightTimes { + if !timeFormatCheck(rt) { + t.Error("wrong time format") + } + } + + wrongTimes := []string{ + ``, // 空时间 + `2020-01-01 abc`, // 含英文字符 + `2020–02-15 23:59:59`, // 2020 后面的不是减号,是个 连接符 + } + for _, wt := range wrongTimes { + if timeFormatCheck(wt) { + t.Error("wrong time format") + } + } +} + // CLA.001 func TestRuleNoWhere(t *testing.T) { common.Log.Debug("Entering function: %s", common.GetFunctionName()) @@ -931,8 +956,15 @@ func TestRuleMeaninglessWhere(t *testing.T) { "select * from tbl where 'a' limit 1;", "select * from tbl where 1;", "select * from tbl where 1 limit 1;", + "select * from tbl where id = 1 or 2;", + "select * from tbl where true;", + "select * from tbl where 'true';", }, { + "select * from tbl where false;", + "select * from tbl where 'false';", + "select * from tbl where 0;", + "select * from tbl where '0';", "select * from tbl where 2 = 1;", "select * from tbl where 'b' = 'a';", }, @@ -3258,17 +3290,24 @@ func TestRuleBlobDefaultValue(t *testing.T) { sqls := [][]string{ { "CREATE TABLE `tb` ( `id` int(10) unsigned NOT NULL AUTO_INCREMENT, `c` blob NOT NULL DEFAULT '', PRIMARY KEY (`id`));", + "CREATE TABLE `tb` (`id` int(10) unsigned NOT NULL AUTO_INCREMENT, `c` json NOT NULL DEFAULT '', PRIMARY KEY (`id`));", "alter table `tb` add column `c` blob NOT NULL DEFAULT '';", + "alter table `tb` add column `c` json NOT NULL DEFAULT '';", }, { "CREATE TABLE `tb` ( `id` int(10) unsigned NOT NULL AUTO_INCREMENT, `c` blob NOT NULL, PRIMARY KEY (`id`));", + "CREATE TABLE `tb` ( `id` int(10) unsigned NOT NULL AUTO_INCREMENT, `c` json NOT NULL, PRIMARY KEY (`id`));", "CREATE TABLE `tb` (`col` text NOT NULL);", "alter table `tb` add column `c` blob NOT NULL;", + "alter table `tb` add column `c` json NOT NULL;", "ALTER TABLE tb ADD COLUMN a BLOB DEFAULT NULL", + "ALTER TABLE tb ADD COLUMN a JSON DEFAULT NULL", "CREATE TABLE tb ( a BLOB DEFAULT NULL)", + "CREATE TABLE tb ( a JSON DEFAULT NULL)", "alter TABLE `tbl` add column `c` longblob;", "alter TABLE `tbl` add column `c` text;", "alter TABLE `tbl` add column `c` blob;", + "alter TABLE `tbl` add column `c` json;", }, } diff --git a/advisor/rules.go b/advisor/rules.go index 0f25b89ce4a5d690685b6050116c4fdf53f3786e..b021eedc91ff0d0fd7fa12ea2fc1d061eb994320 100644 --- a/advisor/rules.go +++ b/advisor/rules.go @@ -531,8 +531,8 @@ func init() { "COL.012": { Item: "COL.012", Severity: "L5", - Summary: "BLOB 和 TEXT 类型的字段不建议设置为 NOT NULL", - Content: `BLOB 和 TEXT 类型的字段无法指定非 NULL 的默认值,如果添加了 NOT NULL 限制,写入数据时又未对该字段指定值可能导致写入失败。`, + Summary: "TEXT、BLOB 和 JSON 类型的字段不建议设置为 NOT NULL", + Content: `TEXT、BLOB 和 JSON 类型的字段无法指定非 NULL 的默认值,如果添加了 NOT NULL 限制,写入数据时又未对该字段指定值可能导致写入失败。`, Case: "CREATE TABLE `tb`(`c` longblob NOT NULL);", Func: (*Query4Audit).RuleBLOBNotNull, }, @@ -556,8 +556,8 @@ func init() { "COL.015": { Item: "COL.015", Severity: "L4", - Summary: "TEXT 和 BLOB 类型的字段不可指定非 NULL 的默认值", - Content: `MySQL 数据库中 TEXT 和 BLOB 类型的字段不可指定非 NULL 的默认值。TEXT最大长度为2^16-1个字符,MEDIUMTEXT最大长度为2^32-1个字符,LONGTEXT最大长度为2^64-1个字符。`, + Summary: "TEXT、BLOB 和 JSON 类型的字段不可指定非 NULL 的默认值", + Content: `MySQL 数据库中 TEXT、BLOB 和 JSON 类型的字段不可指定非 NULL 的默认值。TEXT最大长度为2^16-1个字符,MEDIUMTEXT最大长度为2^32-1个字符,LONGTEXT最大长度为2^64-1个字符。`, Case: "CREATE TABLE `tbl` (`c` blob DEFAULT NULL);", Func: (*Query4Audit).RuleBlobDefaultValue, }, diff --git a/advisor/testdata/TestListHeuristicRules.golden b/advisor/testdata/TestListHeuristicRules.golden index 9731c976ac77046e37fab08c6d254ba2176b6245..7e11549dab5b8070106eb28d09d7ef1ff9ef39b4 100644 --- a/advisor/testdata/TestListHeuristicRules.golden +++ b/advisor/testdata/TestListHeuristicRules.golden @@ -476,7 +476,7 @@ select c1,c2,c3 from tbl where c4 is null or c4 <> 1 * **Item**:COL.012 * **Severity**:L5 -* **Content**:BLOB 和 TEXT 类型的字段无法指定非 NULL 的默认值,如果添加了 NOT NULL 限制,写入数据时又未对该字段指定值可能导致写入失败。 +* **Content**:TEXT、BLOB 和 JSON 类型的字段无法指定非 NULL 的默认值,如果添加了 NOT NULL 限制,写入数据时又未对该字段指定值可能导致写入失败。 * **Case**: ```sql @@ -506,7 +506,7 @@ CREATE TABLE `tb2` ( `id` int(11) DEFAULT NULL, `col` char(10) CHARACTER SET utf * **Item**:COL.015 * **Severity**:L4 -* **Content**:MySQL 数据库中 TEXT 和 BLOB 类型的字段不可指定非 NULL 的默认值。TEXT最大长度为2^16-1个字符,MEDIUMTEXT最大长度为2^32-1个字符,LONGTEXT最大长度为2^64-1个字符。 +* **Content**:MySQL 数据库中 TEXT、BLOB 和 JSON 类型的字段不可指定非 NULL 的默认值。TEXT最大长度为2^16-1个字符,MEDIUMTEXT最大长度为2^32-1个字符,LONGTEXT最大长度为2^64-1个字符。 * **Case**: ```sql diff --git a/ast/testdata/TestSplitStatement.golden b/ast/testdata/TestSplitStatement.golden index 15e7e66158e03eeacf6140deec60b158792e1958..9ad59bac1beb569f741216722c3230b97fc5b1be 100644 --- a/ast/testdata/TestSplitStatement.golden +++ b/ast/testdata/TestSplitStatement.golden @@ -40,6 +40,7 @@ tb; 20 select /*!50000 1,*/ 1; 21 UPDATE xxx SET c1=' LOGGER.error(""); }' WHERE id = 2 ; 22 UPDATE `xxx` SET aaa='a;' WHERE `id` = 15; +23 UPDATE `xxx` SET aaa='a -- b' WHERE `id` = 15; 0 select * from test\G 1 select 'hello\Gworld', col from test\G 2 -- select * from test\Ghello diff --git a/ast/token.go b/ast/token.go index ed8353f421450d8ec130a3e133c9ce1206db082f..432827ca40dba7a3f705a0c7f0658f9281fc9872 100644 --- a/ast/token.go +++ b/ast/token.go @@ -45,7 +45,6 @@ const ( var maxCachekeySize = 15 var cacheHits int var cacheMisses int -var tokenCache map[string]Token var tokenBoundaries = []string{ // multi character @@ -790,7 +789,7 @@ func Tokenize(sql string) []Token { var token Token var tokenLength int var tokens []Token - tokenCache = make(map[string]Token) + tokenCache := make(map[string]Token) // Used to make sure the string keeps shrinking on each iteration oldStringLen := len(sql) + 1 @@ -863,12 +862,12 @@ func SplitStatement(buf []byte, delimiter []byte) (string, string, []byte) { b := buf[i] // single line comment if b == '-' { - if i+2 < len(buf) && buf[i+1] == '-' && buf[i+2] == ' ' { + if !quoted && i+2 < len(buf) && buf[i+1] == '-' && buf[i+2] == ' ' { singleLineComment = true i = i + 2 continue } - if i+2 < len(buf) && i == 0 && buf[i+1] == '-' && (buf[i+2] == '\n' || buf[i+2] == '\r') { + if !quoted && i+2 < len(buf) && i == 0 && buf[i+1] == '-' && (buf[i+2] == '\n' || buf[i+2] == '\r') { sql = "--\n" break } diff --git a/ast/token_test.go b/ast/token_test.go index fe7b4051dedbb95461ec1ecaba608765dfc19d1a..3b4b422e2745be118c7273546fd4f215136ba15c 100644 --- a/ast/token_test.go +++ b/ast/token_test.go @@ -169,11 +169,12 @@ select col from tb where col = 1;`), // 17 select col from tb; select col from tb; `), // 18 - []byte(`INSERT /*+ SET_VAR(foreign_key_checks=OFF) */ INTO t2 VALUES(2);`), // 19 - []byte(`select /*!50000 1,*/ 1;`), // 20 - []byte(`UPDATE xxx SET c1=' LOGGER.error(""); }' WHERE id = 2 ;`), // 21 - []byte("UPDATE `xxx` SET aaa='a;' WHERE `id` = 15;"), // 22 - // []byte(`/* comment here */ SET MAX_JOIN_SIZE=#`), // 23 + []byte(`INSERT /*+ SET_VAR(foreign_key_checks=OFF) */ INTO t2 VALUES(2);`), // 19 + []byte(`select /*!50000 1,*/ 1;`), // 20 + []byte(`UPDATE xxx SET c1=' LOGGER.error(""); }' WHERE id = 2 ;`), // 21 + []byte("UPDATE `xxx` SET aaa='a;' WHERE `id` = 15;"), // 22 + []byte("UPDATE `xxx` SET aaa='a -- b' WHERE `id` = 15; UPDATE `xxx` SET aaa='c -- d' WHERE `id` = 16;"), // 23 + // []byte(`/* comment here */ SET MAX_JOIN_SIZE=#`), // 24 } // \G 分隔符 buf2s := [][]byte{ diff --git a/database/explain.go b/database/explain.go index fae57e581849fd0bc397f7b1ede8df390fa31926..4b5ba088bf644e304673097fe2792f17fe7b9e44 100644 --- a/database/explain.go +++ b/database/explain.go @@ -152,6 +152,7 @@ type ExplainJSONNestedLoop struct { type ExplainJSONBufferResult struct { UsingTemporaryTable bool `json:"using_temporary_table"` NestedLoop []ExplainJSONNestedLoop `json:"nested_loop"` + Table ExplainJSONTable `json:"table"` } // ExplainJSONSubqueries JSON @@ -177,15 +178,17 @@ type ExplainJSONDuplicatesRemoval struct { UsingFilesort bool `json:"using_filesort"` BufferResult ExplainJSONBufferResult `json:"buffer_result"` GroupingOperation ExplainJSONGroupingOperation `json:"grouping_operation"` + Table ExplainJSONTable `json:"table"` } // ExplainJSONOrderingOperation JSON type ExplainJSONOrderingOperation struct { - UsingFilesort bool `json:"using_filesort"` - Table ExplainJSONTable `json:"table"` - DuplicatesRemoval ExplainJSONDuplicatesRemoval `json:"duplicates_removal"` - GroupingOperation ExplainJSONGroupingOperation `json:"grouping_operation"` - OrderbySubqueries []ExplainJSONSubqueries `json:"order_by_subqueries"` + UsingFilesort bool `json:"using_filesort"` + Table ExplainJSONTable `json:"table"` + DuplicatesRemoval ExplainJSONDuplicatesRemoval `json:"duplicates_removal"` + GroupingOperation ExplainJSONGroupingOperation `json:"grouping_operation"` + OrderbySubqueries []ExplainJSONSubqueries `json:"order_by_subqueries"` + OptimizedAwaySubqueries []ExplainJSONSubqueries `json:"optimized_away_subqueries"` } // ExplainJSONQueryBlock JSON diff --git a/database/show.go b/database/show.go index 573ce274766b69ef66061c7a5bb77f45dc7de27b..b81605e5e831285b9a0a50ab5c687f62baa1778c 100644 --- a/database/show.go +++ b/database/show.go @@ -70,6 +70,15 @@ type tableStatusRow struct { Comment []byte // 注释 } +// 记录去除逗号类型是外健还是分区表 +type deleteComaType int8 + +const ( + _ deleteComaType = iota + CS + PART +) + // newTableStat 构造 table Stat 对象 func newTableStat(tableName string) *TableStatInfo { return &TableStatInfo{ @@ -478,21 +487,37 @@ func (db *Connector) ShowCreateTable(tableName string) (string, error) { if len(lines) > 2 { var noConstraint []string relationReg, _ := regexp.Compile("CONSTRAINT") + partitionReg, _ := regexp.Compile("PARTITIONS") + var DeleteComaT deleteComaType for _, line := range lines[1 : len(lines)-1] { if relationReg.Match([]byte(line)) { + DeleteComaT = CS continue + } else if partitionReg.Match([]byte(line)) { + DeleteComaT = PART } line = strings.TrimSuffix(line, ",") noConstraint = append(noConstraint, line) } // 去除外键语句会使DDL中多一个','导致语法错误,要把多余的逗号去除 - ddl = fmt.Sprint( - lines[0], "\n", - strings.Join(noConstraint, ",\n"), "\n", - lines[len(lines)-1], - ) + // len(lines) > 2的判断方式有问题,如果是分区表也会判断成为外键语句,导致建表语句的逗号错乱 + if DeleteComaT == CS { + ddl = fmt.Sprint( + lines[0], "\n", + strings.Join(noConstraint, ",\n"), "\n", + lines[len(lines)-1], + ) + } else if DeleteComaT == PART { + ddl = fmt.Sprint( + lines[0], "\n", + strings.Join(noConstraint, ",\n"), "\n", + lines[len(lines)-3], + ) + } + } + return ddl, err } diff --git a/doc/heuristic.md b/doc/heuristic.md index 27c4a0255cda9e819ff7c7fee30b9197b0ad17e0..ea05609fc8c5ecbbc85d9c114d106d8bffe5e5d0 100644 --- a/doc/heuristic.md +++ b/doc/heuristic.md @@ -202,6 +202,16 @@ INSERT INTO tb (a) VALUES (1), (2) ```sql CREATE TABLE tb (a varchar(10) default '“”' ``` +## IN 条件中存在列名,可能导致数据匹配范围扩大 + +* **Item**:ARG.014 +* **Severity**:L4 +* **Content**:如:delete from t where id in(1, 2, id) 可能会导致全表数据误删除。请仔细检查 IN 条件的正确性。 +* **Case**: + +```sql +select id from t where id in(1, 2, id) +``` ## 最外层 SELECT 未指定 WHERE 条件 * **Item**:CLA.001 @@ -472,11 +482,11 @@ create table tab1(status ENUM('new','in progress','fixed')) ```sql select c1,c2,c3 from tbl where c4 is null or c4 <> 1 ``` -## BLOB 和 TEXT 类型的字段不建议设置为 NOT NULL +## TEXT、BLOB 和 JSON 类型的字段不建议设置为 NOT NULL * **Item**:COL.012 * **Severity**:L5 -* **Content**:BLOB 和 TEXT 类型的字段无法指定非 NULL 的默认值,如果添加了 NOT NULL 限制,写入数据时又未对该字段指定值可能导致写入失败。 +* **Content**:TEXT、BLOB 和 JSON 类型的字段无法指定非 NULL 的默认值,如果添加了 NOT NULL 限制,写入数据时又未对该字段指定值可能导致写入失败。 * **Case**: ```sql @@ -502,11 +512,11 @@ CREATE TABLE tbl( `id` bigint not null, `create_time` timestamp); ```sql CREATE TABLE `tb2` ( `id` int(11) DEFAULT NULL, `col` char(10) CHARACTER SET utf8 DEFAULT NULL) ``` -## TEXT 和 BLOB 类型的字段不可指定非 NULL 的默认值 +## TEXT、BLOB 和 JSON 类型的字段不可指定非 NULL 的默认值 * **Item**:COL.015 * **Severity**:L4 -* **Content**:MySQL 数据库中 TEXT 和 BLOB 类型的字段不可指定非 NULL 的默认值。TEXT最大长度为2^16-1个字符,MEDIUMTEXT最大长度为2^32-1个字符,LONGTEXT最大长度为2^64-1个字符。 +* **Content**:MySQL 数据库中 TEXT、BLOB 和 JSON 类型的字段不可指定非 NULL 的默认值。TEXT最大长度为2^16-1个字符,MEDIUMTEXT最大长度为2^32-1个字符,LONGTEXT最大长度为2^64-1个字符。 * **Case**: ```sql