词法语法分析

词法语法分析

概述

词法分析和语法分析是由Lex和Yacc配合完成,在postgres源代码中为scan.l文件和gram.y文件。这两个文件分别预生成scan.c和gram.c文件,他们俩再配合上词法语法分析模块所需要的C文件就构成了词法分析和语法分析的整个模块。
其中词法分析语法分析需要的文件生成调用关系如下图。
在这里插入图片描述

词法分析器 scan.l 负责识别标识符,SQL 关键字等,对于发现的每个关键字或者标识符都会生成一个记号并且传递给分析器;

语法分析器 gram.y 包含一套语法规则和触发规则时执行的动作;

raw_parser函数(在src/backend/parser/parser.c下)主要通过调用采用Lex和Yacc配合预生成的base_yyparse函数来实现词法分析和语法分析的工作。

重要源文件以及调用关系

源文件 说明
parser.c 词法、语法分析的入口,函数是raw_parse;对查询语句进行语法词法分析后,返回分析树
gram.y 定义语法结构,Yacc语言书写,Lex编译后为gram.c文件
gram.h 定义关键字的数值编号
scan.l 定义词法结构,Lex语言书写,Yacc编译后生成scan.c文件
kwlist.h 定义了关键字列表(Keywords List)。包含了 PostgreSQL 数据库系统中所使用的保留字和关键字的列表。
kwlookup.h 定义SQL语句的keywords(在之前的版本中,该文件还定义了结构体ScanKeyWord)
kwlookup.c 提供ScanKeywordLookup函数,该函数判断输入的字符串是否是关键字,若是则返回当前标识符指向关键字列表中对应单词的指针,采用Hash索引的方法查找(之前的版本采用二分法查找)
scansup.c 提供了几个词法分析用到的函数,downcase_truncate_identifier函数将大写英文字符转换为小写字符,truncate_identifier函数截断超过最大标识符长度的标识符,scanner_isspace函数判断输入字符是否为空白字符。

词法分析器

词法分析通常所做的就是在输入中寻找字符的模式(pattern)。它使用正则表达式匹配输入的字符串并且把它们转换成对应的标记,正则表达式就是一种对模式的简介明了的描述方式。匹配正则表达式的规则,然后执行对应的动作。其实就是提取编程语言占用的各种保留字、操作符、特殊符号等等语言的元素。

语法分析器

语法分析器的任务是找出输入记号之间的关系。一种常见的关系表达式就是语法分析树(parse tree)。

查询语句的执行

SELECT语句在gram.y中的定义

SelectStmt: select_no_parens			%prec UMINUS
			| select_with_parens		%prec UMINUS
		;

select_with_parens:
			'(' select_no_parens ')'				{
    
     $$ = $2; }
			| '(' select_with_parens ')'			{
    
     $$ = $2; }
		;
select_no_parens:
			simple_select						{
    
     $$ = $1; }
			| select_clause sort_clause
				{
    
    
					insertSelectOptions((SelectStmt *) $1, $2, NIL,
										NULL, NULL,
										yyscanner);
					$$ = $1;
				}
			
            ……
			| with_clause select_clause opt_sort_clause select_limit opt_for_locking_clause
				{
    
    
					insertSelectOptions((SelectStmt *) $2, $3, $5,
										$4,
										$1,
										yyscanner);
					$$ = $2;
				}
		;

使用SelectStmt来表示,定义为不带括号(select_no_parens)的和带括号(select_no_parens)的SELECT语句。
不带括号的SELECT语句可以定义为简单的SELECT语句(simple_select),也可以定义成其他的语句如(select_clause)。这对于整个语句的语法分析实际上是将语句拆分成很多个小的语法单元,然后对这些小的语法单元进行分析。

我们用到的是简单SELECT语法

simple_select:
			SELECT opt_all_clause opt_target_list
			into_clause from_clause where_clause
			group_clause having_clause window_clause
				{
    
    
					SelectStmt *n = makeNode(SelectStmt);

					n->targetList = $3;
					n->intoClause = $4;
					n->fromClause = $5;
					n->whereClause = $6;
					n->groupClause = ($7)->list;
					n->groupDistinct = ($7)->distinct;
					n->havingClause = $8;
					n->windowClause = $9;
					$$ = (Node *) n;
				}
			……
		;

simple_select是SELECT语句中最核心的部分,从simple_select的语法中看出,有如下字句:

字句 描述
targetList 目标属性
intoClause SELECT INTO
fromClause FROM字句
whereClause WHERE字句
groupClause GROUP BY字句
havingClause HAVING字句
windowClause 窗口字句

在之前的版本中还有DISTINCT用于去除重复行

成功匹配simple_select语法结构后,会创建一个SelectStmt的结构体

typedef struct SelectStmt
{
    
    
	NodeTag		type;

	/*
	 * 以下字段仅在表示 "叶子" SelectStmts 中使用。
	 */
	List	   *distinctClause; /* NULL,DISTINCT ON 表达式列表,或
								 * lcons(NIL, NIL) 表示所有 (SELECT DISTINCT) */
	IntoClause *intoClause;		/* SELECT INTO 的目标 */
	List	   *targetList;		/* 目标列表(ResTarget 列表) */
	List	   *fromClause;		/* FROM 子句 */
	Node	   *whereClause;	/* WHERE 条件 */
	List	   *groupClause;	/* GROUP BY 子句 */
	bool		groupDistinct;	/* 是否 GROUP BY DISTINCT? */
	Node	   *havingClause;	/* HAVING 条件表达式 */
	List	   *windowClause;	/* WINDOW window_name AS (...), ... */

	/*
	 * 在表示 VALUES 列表的 "叶子" 节点中,上述字段都为 null,代之以这个字段。
	 * 需要注意子列表的元素只是表达式,没有 ResTarget 修饰。
	 * 此外,列表元素可以是 DEFAULT(表示为 SetToDefault 节点),不论 VALUES 列表的上下文如何。
	 * 解析分析将根据是否有效来拒绝该情况。
	 */
	List	   *valuesLists;	/* 未变换的表达式列表的列表 */

	/*
	 * 以下字段在 "叶子" SelectStmts 和上层 SelectStmts 中都使用。
	 */
	List	   *sortClause;		/* 排序子句(SortBy 列表) */
	Node	   *limitOffset;	/* 要跳过的结果元组数 */
	Node	   *limitCount;		/* 要返回的结果元组数 */
	LimitOption limitOption;	/* 限制类型 */
	List	   *lockingClause;	/* FOR UPDATE(LockingClause 列表) */
	WithClause *withClause;		/* WITH 子句 */

	/*
	 * 以下字段仅在上层 SelectStmts 中使用。
	 */
	SetOperation op;			/* 集合操作类型 */
	bool		all;			/* 是否指定 ALL */
	struct SelectStmt *larg;	/* 左子节点 */
	struct SelectStmt *rarg;	/* 右子节点 */
	/* 最终在此处添加用于 CORRESPONDING 规范的字段 */
} SelectStmt;

它定义了 SELECT 查询的各个方面,从目标列表、FROM 子句、WHERE 条件、排序、分组等等。

目标属性

目标属性是SELECT语句中所要查询的属性列表,对应着语法定义中的标识符target_list。target_list由若干个target_el组成,target_list定义为取别名的表达式、表达式以及’*'等

target_list:
			target_el								{
    
     $$ = list_make1($1); }
			| target_list ',' target_el				{
    
     $$ = lappend($1, $3); }
		;

target_el:	a_expr AS ColLabel
				{
    
    
					$$ = makeNode(ResTarget);
					$$->name = $3;
					$$->indirection = NIL;
					$$->val = (Node *) $1;
					$$->location = @1;
				}
			| a_expr BareColLabel
				{
    
    
					$$ = makeNode(ResTarget);
					$$->name = $2;
					$$->indirection = NIL;
					$$->val = (Node *) $1;
					$$->location = @1;
				}
			| a_expr
				{
    
    
					$$ = makeNode(ResTarget);
					$$->name = NULL;
					$$->indirection = NIL;
					$$->val = (Node *) $1;
					$$->location = @1;
				}
			| '*'
				{
    
    
					ColumnRef  *n = makeNode(ColumnRef);

					n->fields = list_make1(makeNode(A_Star));
					n->location = @1;

					$$ = makeNode(ResTarget);
					$$->name = NULL;
					$$->indirection = NIL;
					$$->val = (Node *) n;
					$$->location = @1;
				}
		;

当匹配到target_el时,创建一个ResTarget结构体。该结构体存储了属性的全部信息

typedef struct ResTarget
{
    
    
	NodeTag		type;

	/*
	 * 列名或 NULL
	 */
	char	   *name;

	/*
	 * 下标、字段名和 '*' 的子列表,或 NIL
	 */
	List	   *indirection;

	/*
	 * 要计算或分配的值表达式
	 */
	Node	   *val;

	/*
	 * 标记位置,如果位置未知则为 -1
	 */
	int			location;
} ResTarget;

From子句

from_clause由FROM关键字和from_list组成。而from_list则由若干个标识符table_ref组成,每一个table_ref表示FROM子句中用逗号分隔的每个子项,表示在FROM中出现的一个表或者一个子查询。

from_clause:
			FROM from_list							{
    
     $$ = $2; }
			| /*EMPTY*/								{
    
     $$ = NIL; }
		;

from_list:
			table_ref								{
    
     $$ = list_make1($1); }
			| from_list ',' table_ref				{
    
     $$ = lappend($1, $3); }
		;

table_ref:	relation_expr opt_alias_clause
				{
    
    
					$1->alias = $2;
					$$ = (Node *) $1;
				}
        ……
        ;

FROM子句中的子项(table_ref)最简单和最基本的形式时关系表达式(realation_expr)

relation_expr:
			qualified_name
				{
    
    
					/* inheritance query, implicitly */
					$$ = $1;
					$$->inh = true;
					$$->alias = NULL;
				}
			| extended_relation_expr
				{
    
    
					$$ = $1;
				}
		;
extended_relation_expr:
			qualified_name '*'
				{
    
    
					/* inheritance query, explicitly */
					$$ = $1;
					$$->inh = true;
					$$->alias = NULL;
				}
			| ONLY qualified_name
				{
    
    
					/* no inheritance */
					$$ = $2;
					$$->inh = false;
					$$->alias = NULL;
				}
			| ONLY '(' qualified_name ')'
				{
    
    
					/* no inheritance, SQL99-style syntax */
					$$ = $3;
					$$->inh = false;
					$$->alias = NULL;
				}
		;

关系表达式relation_expr定义成qualified_name、带ONLY关系字的qualified_name等形式,最后qualified_name定义成relation_name。

qualified_name:
			ColId
				{
    
    
					$$ = makeRangeVar(NULL, $1, @1);
				}
			| ColId indirection
				{
    
    
					$$ = makeRangeVarFromQualifiedName($1, $2, @1, yyscanner);
				}
		;

匹配到最终标识符relation_name后创建一个RangeVar结构体来存储关系的信息

typedef struct RangeVar
{
    
    
	NodeTag		type;

	/*
	 * 目录(数据库)名称,或 NULL
	 */
	char	   *catalogname;

	/*
	 * 模式名称,或 NULL
	 */
	char	   *schemaname;

	/*
	 * 关系/序列名称
	 */
	char	   *relname;

	/*
	 * 是否扩展关系的继承?是否递归处理子级?
	 */
	bool		inh;

	/*
	 * 参见 pg_class.h 中的 RELPERSISTENCE_*
	 */
	char		relpersistence;

	/*
	 * 表别名和可选列别名
	 */
	Alias	   *alias;

	/*
	 * 标记位置,如果位置未知则为 -1
	 */
	int			location;
} RangeVar;

关键词查找函数

代码通过计算哈希值并与关键字列表中的哈希值进行比较来执行查找操作。如果哈希值匹配,然后字符逐个比较以检查是否存在精确匹配。。

int
ScanKeywordLookup(const char *str,
				  const ScanKeywordList *keywords)
{
    
    
	size_t		len;
	int			h;
	const char *kw;

	/*
	 * 如果字符串太长以至于不可能是任何关键字,立即拒绝。这样可以避免在长字符串上进行无用的哈希和小写转换操作。
	 */
	len = strlen(str);
	if (len > keywords->max_kw_len)
		return -1;

	/*
	 * 计算哈希函数。我们假设它是生成不区分大小写的结果的。由于它是一个完美哈希函数,只需要匹配它所标识的特定关键字。
	 */
	h = keywords->hash(str, len);

	/* 如果结果超出范围,则表示没有匹配 */
	if (h < 0 || h >= keywords->num_keywords)
		return -1;

	/*
	 * 逐字符比较以查看是否匹配,对输入字符应用基于 ASCII 的小写转换。
	 */
	kw = GetScanKeyword(h, keywords);
	while (*str != '\0')
	{
    
    
		char		ch = *str++;

		if (ch >= 'A' && ch <= 'Z')
			ch += 'a' - 'A';
		if (ch != *kw++)
			return -1;
	}
	if (*kw != '\0')
		return -1;

	/* 成功匹配! */
	return h;
}

在之前的版本中,这个函数使用的是二分查找,使用哈希查找这种技术通常在性能要求高的上下文中使用,以便在大量关键字中快速查找。

在大多数情况下,哈希值匹配更适合用于查找大量关键字的情况,尤其是当查询速度至关重要时。但是,哈希值匹配可能需要一些额外的处理来处理哈希冲突。二分查找适用于已排序的关键字列表,如果不需要频繁地进行插入或删除操作,它可能是一个合适的选择。

猜你喜欢

转载自blog.csdn.net/weixin_47895938/article/details/132457599
今日推荐