pg/og内核原理-词法语法解析(更新中)

本文深入探讨了SQL解析的过程,包括词法分析和语法分析。词法分析通过扫描器(scan.l)按规则切割SQL字符串成词,而语法分析则通过解析器(gram.y)构建状态机匹配SQL结构。解析完成后形成分析树(parsetree),为后续处理提供结构化的数据。此外,还介绍了bison的使用以及plpgsql的特殊词法和语法解析。
摘要由CSDN通过智能技术生成

概念与含义

一条SQL进入数据库的第一步,就是词法语法解析,那么到底什么是词法语法解析、为什么要词法语法解析,词法语法解析后生成了什么?

首先我们思考,为什么要词法语法解析?
其实词法语法解析,换个平易近人的称呼,也可以叫做输入处理。
一条SQL字符串,里面包含的信息很多,查询的表、列、过滤条件、排序条件等等,但是这些信息都存在一个一维的字符串中,机器无法直接获取得到这些信息,因此我们需要对这根字符串进行一些输入处理,处理成方便机器直接处理的形式。什么形式方便机器直接处理?答案:结构体。
我们思考一个结构体 Select 用来表示一个select语句,一个select语句可以包含with子句、from子句、目标列名、where子句…那么这个结构体应该长成如下样子:

struct Select {
    WithClause* with;
    TargetList* targetlist;
    FromClause* from;
    WhereClause* where;
    GroupClasse* groupby;
    ......
}

同理,对于其中每个成员变量,也都构造各自的结构体,用来描述各自的属性,例如表示where子句(where column1 > 100 and …)的WithClause可以定义成

struct WhereClause {
    int num;      // 表达式数量
    Expr* expr;   // 表达式列表
}

那么不难理解,我们会定义出来大量的结构体。
而我们做输入处理的过程,就是将一条SQL字符串进行各种拆解,将其填入这些结构体中,那么这个过程,就是词法语法分析。
如何拆解这个字符串,拆解出来的子串片段又各自按照什么规则填入相关的结构体的那个成员,这个规则就是语法和词法。
将一条SQL进行处理后所形成的这些个结构体,所组成的定是一个树的形态,这棵树,就是分析树(parsetree,有的地方也叫语法树)。

当然,上面这些结构体只是一个描述基本思路的伪代码,实际上的结构体比这要复杂得多,其基本都保存在 src\include\nodes 文件夹下的 parsenodes_common.h parsenodes.h等文件之中,在这里可以很亲切的看到SelectStmt、InsertStmt、DropStmt等。

流程概览

词法解析,就是按照词法规则,将字符串切割拆解成一个个片段,也就是词(token)的过程。一般情况下使用空格作为切割依据,当然也有一些特别的词法,并且对于切割下来的词,会识别这是一个关键字、还是一个标识符等等,这个切割与识别的过程,使用正则表达式完成,代码基本都在scan.l之中。

语法解析,就是使用语法规则,将词填入相关结构体组成parsetree的过程。一条语法由词和子语法组成,子语法又由子子语法和词组成,不严格的说,就是一棵树的形态,叶子节点为词,非叶子为语。然而这只是一条条的语法规则,其具体的实现为通过这一条条规则构造出一个状态机,通过状态的转移,实现语法的匹配。
例如:状态1-create ,状态2-create table,状态3-create index…,当传入的词为create时进入状态1,下一个词为table时进入状态2,为index时进入状态3…
无论时pg还是og,目前都有几千条语法,构造出来的状态机有近万个状态,非常庞大,对于这些语法和相应的状态机,在后面介绍gram.y与bison时我们再做讨论。

因此匹配的过程,就是词法解析在这条SQL字符串上,按照从前到后的顺序,每次切割出来一个词,将词传递给语法解析。语法解析接受这个词,按照语法规则,推动状态机状态转移,之后等待下一个词法的输入。不停的重复这个过程,直到这条SQL字符串切割使用完毕,而最终也停止在某个结束状态,即最终匹配出了一条语法。
其中任意一次状态转移失败,或者最终没有停在终止状态时,都属于语法错误。

Created with Raphaël 2.3.0 开始 切割SQL字符串,取一个词 匹配语法,推进状态机 切割完毕? 结束 yes no

例如,对于create table tbname …的解析:
存在如下状态:
status0:.
select … to …
create … to status 100
insert … to …

status100:create .
table … to status 288
index … …
user … …

status288:create table .
IDENT …

首先处于状态0,状态0下可以接受的动作列表中发现有create ,且接受到create之后会转移到状态100.
接受create,转移至状态100,下一步的状态列表中有table关键字,接受table关键字,转移至状态288.

熟悉编译原理、lex、bison的小伙伴肯定已经很清楚这个流程。
当然,这些流程都是bison原理,状态机也是bison构造而来,我们不需要详细了解其中的每个细节,只需要知道我们给bsion、lex的输入就可以了,也就是gram.y、scan.l。

词法语法代码

词法

parsenode.h

在第一章节的概念部分,我们已经知道了我们需要一些结构体,用来描述一个SQL的各个部分,并组成parsetree,这些结构体主要都在这个文件里面,当然其他的头文件里也有一些。
这一部分就不多赘述了,理解了parsetree的概念之后,这一部分很好理解。

kwlist.h

对于关键词,我们将其存放在这个文件之中,内部是一个列表,列出了所有的关键词。结构大致如下:

关键词分为种:保留关键词、非保留关键词、函数与类型名特别关键词、表名列名特别关键词
对于这四种关键词,各自有各自不同的限制和作用,尤其是后两种,乍一看一头雾水,那么其究竟是什么呢?等我们了解完gram.y之后就自然而然清楚了。

scan.l

scan.l里面时存放的就是词法规则,其是围绕正则表达式来实现的。例如在这里我们能够看到非常熟悉的标识符规则:

ident_start		[A-Za-z\200-\377_]
ident_cont		[A-Za-z\200-\377_0-9\$\#]
identifier		{ident_start}{ident_cont}*

可以看到其符合数字下划线开头,只能包含数字字母下划线以及俩符号。
上面这个是直接由正则表达式来完成的,还有一些复杂一些的是由很多部分的正则表达式完成,比如下面这个稍微复杂一点的,e转义词法(如: select e’abcd\n’; )的部分相关代码:

部分精简代码摘抄:
%x xe

quote			'
quotestop		{quote}{whitespace}*
quotecontinue	{quote}{whitespace_with_newline}{quote}
quotefail		{quote}{whitespace}*"-"

xestart			[eE]{quote}
xeinside		[^\\']+
xeescape		[\\][^0-7]
xeoctesc		[\\][0-7]{1,3}
...

{xestart}		{	...;   BEGIN(xe);	...;   }
<xe>{xeinside}  {	...   }
<xq,xe>{quotestop}	{   ...  return SCONST; }
<xe>{xeunicode} {	...   }
<xe>{xeoctesc} {	...   }
...

可以看出来,其涉及到了三个部分,状态、正则表达式、触发动作。
其中 %x xe 表示状态,其含义为当前正在解析一个e转移的词法。
正则表达式则为对输入进行的匹配。
触发动作则为某种状态下匹配成功一个正则表达式之后的回调动作。

则e转义的词法解析流程为
1、对于前两个字符 e’ 匹配成功xestart正则表达式,并触发xestart的触发动作,可以看到其函数体内将当前状态标识为了xe状态。
2、之后每次匹配成功相应的正则表达式,如xeoctesc、xeescape等,都会触发相应的回调动作。对于有时可能会一段词满足多个正则表达式的,则会有状态来进行约束,一种状态下只会有一种正则表达式满足,并触发其动作。
3、匹配到了quotestop正则表达式,触发动作,动作里面词法解析完成,结束返回SCONST。

gram.y

这里面存放的就是所有的语法规则。在前面我们已经知道了一条sql是怎么通过bison状态机来解析的,并且状态机是通过gram.y进行构造的,那么现在我们便来了解一下gram.y的内容以及格式。

gram.y分为语法

bison的使用

bison --help 或者查看bison官网,了解更多的bison用法。

查看状态机

bison的参数-o

解决规约冲突

plpgsql的词法与语法

存储过程的词法语法,有专门的两个文件plgram.y, plscan.l来进行解析。
那么这个这两个文件又是如何与前面的gram.y、scan.l是如何配合的?CreateFuncStmt语法在gram.y里已经有了,那为何还要一个专门的文件?而且也没有看到include相关的操作。

我们知道,create function的时候,函数体通常被一对 $$ 符号所包裹起来,其实这个符号的含义就是一个起止符号,在这对符号之间,所有的内容只会被认为是一个字符串,也就是一个词,一个SCONST。
在gram.y中,对于CreateFuncStmt语法部分的函数体的定义比较模糊,只是一个简单的词,因此,在这一步不回去解析函数体部分,函数体还是以sql字符串的形式存放在parsetree中。

正常走过计划、portal、走到processUtility之中,会调用functionDefine进行创建函数,在这个函数里会调用plpgsql_parse_query函数对一路传下来的字符串进行解析,而这个函数内部调用的便是由plgram.y\plscan.l进行编译的来的了。

  • 1
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值