16

TiDB源码阅读(二) 简单理解一下 Lex & Yacc

 3 years ago
source link: https://studygolang.com/articles/30110
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

上一篇中,介绍了 TiDB 的入口,从根据配置启动 TiDB 到匹配 MySQL 协议,再到开始做 parser。那接下来我们就简单了解下 SQL 解析处理这一块的内容。

当我还是萌新的时候,参与过 Java SQL 解析、优化器 demo 的编写,不过也只是聊到用的技术是 ANTRL ,甚至不知道为什么要做解析、优化,也不大了解是什么原理实现。

最新学习 TiDB 解析优化 SQL 的流程,深觉还是要先至少简单的了解 Lex & Yacc

它们能够让你更容易的解析复杂的语言,达成解析字符串的目的。

Lex & Yacc

输入字符流 ,发现某一段字符能够匹配一个关键字,就根据定义好的动作来执行。

例 1 打印

%%
begin    printf("BEGIN;\n");
executeSql    printf("SELECT * FROM t1;\n");   
commit   printf("COMMIT;\n");
%%

Lex 的每一段是通过 %% 分割的,这里设置了 3 个关键字 :

begin
executeSql
commit

读取字符流时,遇到关键字 ,就会根据后面的指令去执行动作。比如遇到 executeSql ,会print " SELECT * FROM t1 ; " 如果匹配不到关键字 ,会正常输出。

例 2 解析日志

[2020/07/31 09:43:01] 
[INFO] 
[server.go:391] 
["connection closed"] 
[conn=4]

根据日志中的元素,定义如下关键字

WORD > connection|conn|INFO|closed
DATE > 2020/07/31 09:43:01
FILENAME > server.go
NUM > 391|4
LEFTBRACKET > [
RIGHTBRACKET > ]
COLON > :
SLASH > /
EQUALSIGN > =
QUOTATIONMARK > "

Lex 分词器

%%
[a−zA−Z][a−zA−Z0−9]*        return WORD 
日期的正则表达式.手动狗头       return DATE
\[a−zA−Z0−9\/.−]+           return FILENAME
\[0123456789]+              return NUM 
\[                          return LEFTBRACKET
\]                          return RIGHTBRACKET
\:                          return COLON 
\/                          return SLASH 
\=                          return EQUALSIGN 
\"                          return QUOTATIONMARK 
%%

经过 Lex 分词的结果就是

LEFTBRACKET DATE RIGHTBRACKET
LEFTBRACKET WORD RIGHTBRACKET
LEFTBRACKET FILENAME COLON NUM RIGHTBRACKET
LEFTBRACKET QUOTATIONMARK WORD WORD QUOTATIONMARK RIGHTBRACKET 
LEFTBRACKET WORD EQUALSIGN NUM RIGHTBRACKET

在 TiDB 中,类似的结构都存放在 parser.y 中,

结构如下,

第一部分主要是定义 Token 的类型、优先级、结合性等。

%{
package parser
import ( 
    "strings"
    "github.com/pingcap/parser/mysql"
    "github.com/pingcap/parser/types"
)
%}
%union {
    offset int // offset
    item interface{}
    ident string
    expr ast.ExprNode
    statement ast.StmtNode
}
%token <ident>
%type  <expr>
%precedence empty
%left join straightJoin inner cross left right full natural
%start    Start

通过 %% 分割,以上是第一部分,即定义段

%%

下部分是 SQL 语法的产生式和每个规则对应的 action ,我们找一个简单的看看,

这应该是 Drop Table 的 分词结构,生成 ast.DropTableStmt 语法树来执行

DropTableStmt:
"DROP" OptTemporary TableOrTables IfExists TableNameList RestrictOrCascadeOpt
{
    $$ = *.DropTableStmt{IfExists: $4.(bool), Tables: $5.([]*ast.TableName), IsView: false, IsTemporary: $2.(bool)}
}

这里有 5 个 Token ,分别是

OptTemporary
TableOrTables
IfExists
TableNameList
RestrictOrCascadeOpt

分别看一下这些 Token 的定义,那两个 Table 巴拉巴拉就不看了

OptTemporary

//应该是临时表的 Token ,如果有这个 Token ,则会被解析。
//但也如逻辑中写的,“TiDB 目前不支持临时表,虽然会被解析,但是不生效。”
OptTemporary:
    /* empty */
    {
        $$ = false
    }
|    "TEMPORARY"
    {
        $$ = true
        yylex.AppendError(yylex.Errorf("TiDB doesn't support TEMPORARY TABLE, TEMPORARY will be parsed but ignored."))
        parser.lastErrorAsWarn()
    }

if exists

// 是否有 if exists
IfExists:
    {
        $$ = false
    }
|    "IF" "EXISTS"
    {
        $$ = true
    }

restrict: 确保只有不存在相关视图和完整性约束的表才能删除

RestrictOrCascadeOpt:
    {}
|    "RESTRICT"
|    "CASCADE"

所以可以看出,在 drop table 的时候,在这个语法结构中," 丰满 " 的语句大概是

drop temporary table Ifexists tablename restrict/cascade

之后就会生成一棵 ast 抽象语法 ast.DropTableStmt 。

ast/ddl.go

type DropTableStmt struct {
    ddlNode

    IfExists    bool
    Tables      []*TableName
    IsView      bool
    IsTemporary bool // make sense ONLY if/when IsView == false
}

这个具体的实现,比如这个

func (n *DropTableStmt) Restore(ctx *format.RestoreCtx) error {
    if n.IsView {
        ctx.WriteKeyWord("DROP VIEW ")
    } else {
        if n.IsTemporary {
            ctx.WriteKeyWord("DROP TEMPORARY TABLE ")
        } else {
            ctx.WriteKeyWord("DROP TABLE ")
        }
    }
    if n.IfExists {
        ctx.WriteKeyWord("IF EXISTS ")
    }
    for index, table := range n.Tables {
        if index != 0 {
            ctx.WritePlain(", ")
        }
        if err := table.Restore(ctx); err != nil {
            return errors.Annotate(err, "An error occurred while restore DropTableStmt.Tables "+string(index))
        }
    }

    return nil
}

先判断了 drop 的是 view 还是 table , 如果走进了 table 分支,也就大概判断了是否是临时表,是否有各种特殊的语法。

到这里基本了解了 TiDB 中对于 SQL 解析的方式,当然,和行文的区别, TiDB 用的是 goyacc,不过好像区别也不是很大,这篇希望可以给大家一个参考。

有疑问加站长微信联系

iiUfA3j.png!web

About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK