parser 解析 function 请教

为提高效率,请提供以下信息,问题描述清晰能够更快得到解决:

【概述】 场景 + 问题概述
我尝试在对 parser 进行修改,希望其能支持对 function 的解析,在解析部分 sql 的时候有些疑惑,请各位大佬指教。

【parser 版本】
v4.0.2

【步骤】

/********************************************************************
* Create Function Statement
 *
 * CREATE
 *     [DEFINER = user]
 *     FUNCTION sp_name ([func_parameter[,...]])
 *     RETURNS type
 *     [characteristic ...] routine_body
 *
 * func_parameter:
 *     param_name type
 *
 * type:
 *     Any valid MySQL data type
 *
 * characteristic: {
 *     COMMENT 'string'
 *   | LANGUAGE SQL
 *   | [NOT] DETERMINISTIC
 *   | { CONTAINS SQL | NO SQL | READS SQL DATA | MODIFIES SQL DATA }
 *   | SQL SECURITY { DEFINER | INVOKER }
 * }
 *
 * routine_body:
 *     Valid SQL routine statement
 *
 * Ref:
 *    https://dev.mysql.com/doc/refman/8.0/en/create-procedure.html
 *******************************************************************/
CreateFunctionStmt:
	"CREATE" "FUNCTION" FunctionName FunctionParameterOpt ReturnDataOpt CharacteristicOptionList
	{
		x := &ast.CreateFunctionStmt{
			Name: $3.(*ast.TableName),
			Args: $4.([]*ast.ColumnDef),
			Rtp:  $5.(*types.FieldType),
		}
		if $6 != nil {
			x.Characteristic = $6.(*ast.CharacteristicOption)
		}
		$$ = x
	}

CharacteristicOptionList:
	/* empty */
	{
		$$ = nil
	}
|	CharacteristicOptionList CharacteristicOption
	{
		// Merge the options
		if $1 == nil {
			$$ = $2
		} else {
			opt1 := $1.(*ast.CharacteristicOption)
			opt2 := $1.(*ast.CharacteristicOption)
			if len(opt2.Comment) > 0 {
				opt1.Comment = opt2.Comment
			} else if opt2.Deterministic != opt1.Deterministic {
				opt1.Deterministic = opt2.Deterministic
			} else if opt2.Security != opt1.Security {
				opt1.Security = opt2.Security
			}
			$$ = opt1
		}
	}

CharacteristicOption:
	{
		$$ = nil
	}
|	"COMMENT" stringLit
	{
		$$ = &ast.CharacteristicOption{
			Comment: $2,
		}
	}
|	DeterministicOrNotOp
	{
		$$ = &ast.CharacteristicOption{
			Deterministic: $1.(bool),
		}
	}
|	ViewSQLSecurity
	{
		$$ = &ast.CharacteristicOption{
			Security: $1.(model.ViewSecurity),
		}
	}

DeterministicOrNotOp:
	"DETERMINISTIC"
	{
		$$ = true
	}
|	"NOT" "DETERMINISTIC"
	{
		$$ = false
	}

FunctionName:
	TableName

FunctionParameterOpt:
	/* empty */
	{
		$$ = make([]*ast.ColumnDef, 0, 1)
	}
|	'(' FunctionColumnDefList ')'
	{
		$$ = $2.([]*ast.ColumnDef)
	}

FunctionColumnDefList:
	FunctionColumnDef
	{
		$$ = []*ast.ColumnDef{$1.(*ast.ColumnDef)}
	}
|	FunctionColumnDefList ',' FunctionColumnDef
	{
		$$ = append($1.([]*ast.ColumnDef), $3.(*ast.ColumnDef))
	}

FunctionColumnDef:
	ColumnName Type
	{
		colDef := &ast.ColumnDef{Name: $1.(*ast.ColumnName), Tp: $2.(*types.FieldType)}
		$$ = colDef
	}

ReturnDataOpt:
	"RETURNS" Type
	{
		$$ = $2.(*types.FieldType)
	}

ddl.go 中代码如下

// CreateFunctionStmt is a statement to create a Function.
type CreateFunctionStmt struct {
	ddlNode

	IfNotExists bool
	Name        *TableName
	Args        []*ColumnDef
	Rtp         *types.FieldType

	Characteristic *CharacteristicOption
}

// Restore implements Node interface.
func (n *CreateFunctionStmt) Restore(ctx *format.RestoreCtx) error {
	ctx.WriteKeyWord("CREATE ")
	ctx.WriteKeyWord("FUNCTION ")
	if n.IfNotExists {
		ctx.WriteKeyWord("IF NOT EXISTS ")
	}
	if err := n.Name.Restore(ctx); err != nil {
		return errors.Annotate(err, "An error occurred while create CreateFunctionStmt.Name")
	}
	if len(n.Args) > 0 {
		ctx.WritePlain("(")
		for i, col := range n.Args {
			if i > 0 {
				ctx.WritePlain(",")
			}
			if err := col.Restore(ctx); err != nil {
				return errors.Annotatef(err, "An error occurred while splicing CreateFunctionStmt Args: [%v]", i)
			}
		}

		ctx.WritePlain(")")
	}
	ctx.WritePlain(" RETURNS ")
	if err := n.Rtp.Restore(ctx); err != nil {
		return errors.Annotatef(err, "An error occurred while splicing CreateFunctionStmt Rtp: [%v]", n.Rtp)
	}
	if n.Characteristic != nil {
		ctx.WritePlain(" ")
		if err := n.Characteristic.Restore(ctx); err != nil {
			return errors.Annotatef(err, "An error occurred while splicing CreateFunctionStmt Characteristic: [%v]", n.Characteristic)
		}
	}

	return nil
}

// Accept implements Node Accept interface.
func (n *CreateFunctionStmt) Accept(v Visitor) (Node, bool) {
	newNode, skipChildren := v.Enter(n)
	if skipChildren {
		return v.Leave(newNode)
	}
	n = newNode.(*CreateFunctionStmt)
	node, ok := n.Name.Accept(v)
	if !ok {
		return n, false
	}
	n.Name = node.(*TableName)
	return v.Leave(n)
}

// CharacteristicOption ...
// CharacteristicOption for function or procedure
type CharacteristicOption struct {
	node

	Comment       string
	Deterministic bool
	Security      model.ViewSecurity
}

// Accept implements Node Accept interface.
// Accept implements Node Accept interface.
func (n *CharacteristicOption) Accept(v Visitor) (Node, bool) {
	newNode, skipChildren := v.Enter(n)
	if skipChildren {
		return v.Leave(newNode)
	}
	n = newNode.(*CharacteristicOption)
	return v.Leave(n)
}

// Restore implements Node interface.
func (n *CharacteristicOption) Restore(ctx *format.RestoreCtx) error {
	hasPrevOption := false
	if n.Comment != "" {
		if hasPrevOption {
			ctx.WritePlain(" ")
		}
		ctx.WriteKeyWord("COMMENT ")
		ctx.WriteString(n.Comment)
		hasPrevOption = true
	}
	if n.Deterministic {
		if hasPrevOption {
			ctx.WritePlain(" ")
		}
		ctx.WriteKeyWord("DETERMINISTIC")
		hasPrevOption = true
	} else {
		if hasPrevOption {
			ctx.WritePlain(" ")
		}
		ctx.WriteKeyWord("NOT DETERMINISTIC")
		hasPrevOption = true
	}

	ctx.WriteKeyWord(" SQL SECURITY ")
	ctx.WriteKeyWord(n.Security.String())

	return nil
}

在执行 make 的时候,出现了

make
gofmt (simplify)
bin/goyacc -o parser.go -p yy -t Parser parser.y
conflicts: 4 shift/reduce
conflicts: 6 reduce/reduce

请问,我该怎么处理 CharacteristicOptionList 这个值?

1 个赞

TiDB 开发者社区小伙伴已经在看了,请稍等哈

一般 shift/reduce reduce/reduce 这种问题都是由于语法定义里面有冲突导致的

有 github 上的代码么?我需要把 parser 的改动拉下来看一下

好的,稍后我把代码放出来。

https://github.com/recall704/parser 对应 branch 为 function (https://github.com/recall704/parser/tree/function)

不对呀,function 这个branch 相对于 pingcap/master 没有改动呀?

sorry,现在有了

你可以把 list 改写成这种形式:

CharacteristicOptionList:
CharacteristicOption
| CharacteristicOptionList CharacteristicOption

list 就是1个或者多个,不能够为空

再然后把 CharacteristicOption 改掉,不要让它跟 ViewSQLSecurity 同时可以以 empty 开头,这样 yacc 语法没法决定使用哪一条规则,会出现 reduce / shift 那个报错

尽量让每个规则都很明确,比如

List
Item
| List Item

list 为 1个或者多个元素,而不要搞零个元素的,然后如果是可以为零个的,就加另一条规则

ListOpt
{}
| List

我现在的疑惑在于

  • CharacteristicOptionList 整个为 empty 的时候,我的 empty 应该在下面的每个子项中处理,还是应该是 empty | CharacteristicOptionList
  • 在每一个子项中,都有可能是 empty,多个 empty 同时出现就有可能出现 shift/reduce 错误,多个 empty 应该如何处理

你要理解 shift/reduce 发生的原因,是 yacc 不知道使用哪一条规则了,语法部分定义的有歧义

我的 empty 应该在下面的每个子项中处理,还是应该是 empty | CharacteristicOptionList

语法书写者不知道放到上层处理,还是子项处理的时候,那 yacc 工具更不会知道。
你可以随便怎么弄,让它没有歧义就行了。

这里有一个例子, https://github.com/recall704/parser/pull/3

之间你的代码里面的歧义,主要是 CharacteristicOptionList 可以为空,而子项 ViewSQLSecurity 那一条也是一个可以为空的,于是工具就不知道用哪条规则了。我改了一下让 ViewSQLSecurity 子项不会为空,就可以了。

1 个赞

好的,非常感谢。

我先尝试一下。

非常感谢您的帮助,之前的问题都已经处理了。

现在我在处理 RoutineBody 的时候遇到了新的问题,我尝试过以下的方式解析:

  • TextString,期望把 BEGIN END 之间的内容完全解析为 字符串
  • ExprNode,期望把 BEGIN END 之间的内容完全解析为 通用的 sql

以上测试都失败了,这部分能给一些建议吗?

这是我新提交的代码: https://github.com/recall704/parser/pull/4/files

如果 routine body 是非字符串的内容,是不能解析为字符串的
因为 lexer 那一层,只把引号括起来的东西 ‘string’ 识别为字符串 token。如果 routine body 是 ’ a valid sql’ 这种写法才能够解析成字符串。

我看到你为了解析成 expression,在 ExpressionList 前面添加了 BEGIN END 的 token,你是希望解析成 Statement 吧?

genius@genius-System-Product-Name:~/project/src/github.com/pingcap/parser$ git diff parser.y
diff --git a/parser.y b/parser.y
index d01e992..69caf7e 100644
--- a/parser.y
+++ b/parser.y
@@ -5607,7 +5607,7 @@ UnReservedKeyword:
 |      "DUPLICATE"
 |      "DYNAMIC"
 |      "ENCRYPTION"
-|      "END"
+/* |   "END" */
 |      "ENFORCED"
 |      "ENGINE"
 |      "ENGINES"
@@ -13201,7 +13201,7 @@ CreateFunctionStmt:
        }
 
 RoutineBody:
-       "BEGIN" ExpressionList "END"
+       "BEGIN" ExplainableStmt "END"
        {
                $$ = $2
        }

我发现还不太好弄,主要的冲突是 select statement 里面,field 也可以是 end ,比如 select end …
跟这里的 end 出现歧义了,一个不太正确的改法可以尝试把 end 从 UnReserved keyword 里面拿掉

或者正常的改法,尝试一下设置 precedence,这块我也了解不深入,需要摸索一下
https://www.ibm.com/docs/en/zos/2.3.0?topic=section-precedence-in-grammar-rules

我的目的是希望能把 RoutineBody 解析为一个通用的 stmtNode, 在 Restore 的时候能够正常还原即可。

但是按照通用的方式,我需要编写一个 BeginEndStmt,然后 BeginEndStmt 中又需要实现

  • DeclareStmt
  • IfElseStmt
  • DeclareStmt
  • CaseWhenStmt
  • IfCaseStmt

等等,如果每个都需要实现(这样理解没错吧?),代价有点大。

我这里更期望的是像 CreateViewStmt 中的 CreateViewSelectOpt 一样

	startOffset := parser.startOffset(&yyS[yypt-1])
	selStmt := $10.(ast.StmtNode)
	selStmt.SetText(strings.TrimSpace(parser.src[startOffset:]))

避免重定义,需要复用以前的 Stmt,你参考下 ExplainableStmt 的定义。

如果你想弄成 string 然后再解析一遍,那参考 CreateViewSelectcOpt 也可以

另外,Statement 跟 Expression 是不同的东西,上面列举的那些都是 Expression。

此话题已在最后回复的 1 分钟后被自动关闭。不再允许新回复。