MySQL SQL文件审核定位行数错误
verison
UI: main aeee2f9d DMS: main-ee 19ee2c1ea2 SQLE: main-ee 342c635080
错误复现
发现两个错误
1. 不能解析的SQL会被认为是同一行(github不能上传.sql文件,将下面的sql复制到sql文件复现)
create table point_trans_shard_00_part_202401(like point_trans_shard_00 including all) inherits(point_trans_shard_00);
Alter table point_trans_shard_00_part_202401 ADD CONSTRAINT chk_point_trans_shard_202401 CHECK (processedtime >= '1704038400000'::bigint AND processedtime < '1706716800000'::bigint );
create table point_trans_source_shard_00_part_202401(like point_trans_source_shard_00 including all) inherits(point_trans_source_shard_00);
Alter table point_trans_source_shard_00_part_202401 ADD CONSTRAINT chk_point_trans_source_shard_202401 CHECK (processedtime >= '1704038400000'::bigint AND processedtime < '1706716800000'::bigint );
grant all on point_trans_shard_00_part_202401 to kgoldpointapp;
grant select on point_trans_shard_00_part_202401 to prd_fin, dbsec, sec_db_scan;
grant all on point_trans_source_shard_00_part_202401 to kgoldpointapp;
grant select on point_trans_source_shard_00_part_202401 to prd_fin, dbsec, sec_db_scan;
2. sql末尾有“;”的sql,影响行数错误
grant all on point_trans_shard_00_part_202401 to kgoldpointapp;
grant select on point_trans_shard_00_part_202401 to prd_fin, dbsec, sec_db_scan;
grant all on point_trans_source_shard_00_part_202401 to kgoldpointapp;;
grant select on point_trans_source_shard_00_part_202401 to prd_fin, dbsec, sec_db_scan;
3 当有语法错误时,展示的行数应该是sql的起始行数,而不是语法出错的行数
有语法错误的sql起始行数是第2行
这里应该显示sql定位行数为2,但显示为出错的行数
问题1原因
问题背景:问题一中的第一到四条sql都属于解析器无法解析的sql,第5到8条属于解析器可以解析的sql.
当解析器遇到不能解析的sql时,会走到图1不能解析sql的逻辑,在这个逻辑中,会将Line值设为默认值1,遇到\n时解析器会默认将Line值加1,每次遇到解析解析器不支持的sql时,解析器都会将Line值设为1,所以第二到四条sql的行数都为2.
问题1解决方案
每次当解析器遇到不能解析的sql时,累加startLineOffset行数的值,这样就可以解决问题1
问题1影响面
问题1中的开始行数显示正常
问题3原因
因为为Line值是由\n的个数决定的,问题3的sql存在两个\n,所以Line值为3,导致页面行号行号显示为3
问题3解决方案
问题3备注
背景
目前使用的是 parser.lexer.r.pos().Line 的值用作当前行号,parser.lexer.r.pos().Line的值实际是指当前解析的字符的行号,而不是开始行数.所以当遇到有换行符且不能解析的sql时, 解析器的行号会停在出错的行号,导致行号不准确.例如有如下sql,解析器会停在第3行,而不是第2行,导致开始行号不准确
select 1;
select *
fromd
tt;
一些尝试
思路
既然通过解析器的 parser.lexer.r.pos().Line 值来获取开始行号不准确,那么可以换一种思路,先获取当前行数解析点之前的sql, 然后计算当前解析点之前sql的总的行号,再加上当前解析的sql的换行符符前缀数量,就是当前sql的开始行号.
实现步骤
- Parser struct 增加 endLineOffset 字段,用于记录当前解析点之前的sql的总行数
type Parser struct {
charset string
collation string
result []ast.StmtNode
src string
lexer Scanner
hintParser *hintParser
// the following fields are used by yyParse to reduce allocation.
cache []yySymType
yylval yySymType
yyVAL *yySymType
startLineOffset int
endLineOffset int
}
- 实现思路 (perfect_parser.go) 实现思路如下, 通过解析器的解析结果,获取当前解析点之前的sql的总行数,然后计算当前解析的sql的开始行号. 但是这种思路依赖对解析器的解析结果进行分析处理,处理能力有限且容易出错,我觉得解决思路还是应该在解析器内部实现.
package parser
import (
"bytes"
"github.com/pingcap/parser/ast"
)
// PerfectParse parses a query string to raw ast.StmtNode. support parses query string
// who contains unparsed SQL, the unparsed SQL will be parses to ast.UnparsedStmt.
func (parser *Parser) PerfectParse(sql, charset, collation string) (stmt []ast.StmtNode, warns []error, err error) {
_, warns, err = parser.Parse(sql, charset, collation)
stmts := parser.result
parser.updateStartLineWithOffset(stmts)
if err == nil {
return stmts, warns, nil
}
// if err is not nil, the query string must be contains unparsed sql.
if len(stmts) > 0 {
for _, stmt := range stmts {
ast.SetFlag(stmt)
}
stmt = append(stmt, stmts...)
}
// The origin SQL text(input args `sql`) consists of many SQL segments,
// each SQL segments is a complete SQL and be parsed into `ast.StmtNode`.
//
// good SQL segment bad SQL segment
// |---------------------|---------------------|---------------------|---------------------| origin SQL text
// ^ ^
// stmtStartPos lastScanOffset
// |------|---------------------|---------------------| remaining SQL text
//
// |< unparsed stmt >|< continue to parse it >|
start := parser.lexer.stmtStartPos
cur := parser.lexer.lastScanOffset
remainingSql := sql[cur:]
l := NewScanner(remainingSql)
var v yySymType
var endOffset int
var scanEnd = 0
var defaultDelimiter int = ';'
delimiter := defaultDelimiter
ScanLoop:
for {
result := l.Lex(&v)
switch result {
case scanEnd:
endOffset = l.lastScanOffset - 1
break ScanLoop
case delimiter:
endOffset = l.lastScanOffset
break ScanLoop
case begin:
// ref: https://dev.mysql.com/doc/refman/8.0/en/begin-end.html
// ref: https://dev.mysql.com/doc/refman/8.0/en/stored-programs-defining.html
// Support match:
// BEGIN
// ...
// END;
//
delimiter = scanEnd
case end:
// match `end;`
var ny yySymType
next := l.Lex(&ny)
if next == defaultDelimiter {
delimiter = defaultDelimiter
endOffset = l.lastScanOffset
break ScanLoop
}
case invalid:
// `Lex`内`scan`在进行token遍历时,当有特殊字符时返回invalid,此时未调用`inc`进行滑动,导致每次遍历同一个pos点位触发死循环。有多种情况会返回invalid。
// 对于解析器本身没影响,因为 token 提取失败就退出了,但是我们需要继续遍历。
if l.lastScanOffset == l.r.p.Offset {
l.r.inc()
}
}
}
unparsedStmtBuf := bytes.Buffer{}
unparsedStmtBuf.WriteString(sql[start:cur])
unparsedStmtBuf.WriteString(remainingSql[:endOffset+1])
// 不能解析sql之前的sql的总行数
if start != 0 {
parser.endLineOffset += getLineNumber(sql, start)
}
unparsedSql := unparsedStmtBuf.String()
// 当前sql的开始行号parser.endLineOffset加上当前sql的换行符前缀数量countNewLinePrefix(unparsedStmtBuf.String())
// 获得当前sql的开始行号
parser.startLineOffset = parser.endLineOffset + countNewLinePrefix(unparsedStmtBuf.String()) - 1
// 如果出现所有sql都是无法解析的sql,则当前sql的开始行号startLineOffset为0,结束行号endLineOffset为1
if parser.endLineOffset == 0 && countNewLinePrefix(unparsedStmtBuf.String()) == 0 {
parser.startLineOffset = 0
parser.endLineOffset = 1
}
// 更新结束行号
parser.endLineOffset += getTotalLine(unparsedStmtBuf.String())
if len(unparsedSql) > 0 {
un := &ast.UnparsedStmt{}
un.SetStartLine(parser.startLineOffset + 1)
un.SetText(unparsedSql)
stmt = append(stmt, un)
}
if len(remainingSql) > endOffset {
cStmt, cWarn, cErr := parser.PerfectParse(remainingSql[endOffset+1:], charset, collation)
warns = append(warns, cWarn...)
if len(cStmt) > 0 {
stmt = append(stmt, cStmt...)
}
if cErr == nil {
return stmt, warns, cErr
}
}
return stmt, warns, nil
}
func (parser *Parser) updateStartLineWithOffset(stmts []ast.StmtNode) {
for i := range stmts {
stmts[i].SetStartLine(stmts[i].StartLine() + parser.startLineOffset)
}
}
func getTotalLine(remainingSql string) int {
count := 0
for _, char := range remainingSql {
if char == '\n' {
count++
}
}
return count
}
func getLineNumber(s string, pos int) int {
if pos == 0 {
return 0
}
lineNumber := 0
for i := 0; i <= pos; i++ {
if s[i] == '\n' {
lineNumber++
}
}
return lineNumber
}
func countNewLinePrefix(s string) int {
count := 0
for _, char := range s {
if char == '\n' {
count++
} else {
break
}
}
return count
}
处理方案考虑 把处理行数的逻辑从解析器中剥离出来,和解析器解耦合,方便解析器升级
相关issue: #2354 #2406