引言
在上一篇博客中我对与parser语法解析过程中与参数处理相关的文件parse_param.cpp进行了学习和解析。事实上,在查询语句解析的过程中,词法解析、语法解析、语义解析等各模块之间并不是孤立的,而是具有相当强的关联性;在对parse_param.cpp的文件内容进行解读的过程中我发现文件已经涉及到许多与语义解析过程有关的数据结构和参数处理过程。在本篇博客中,我将开始对parser语义解析模块的学习和相关文件analyze.cpp内容的解读。
文件路径
src/common/backend/parser/analyze.cpp
语义解析简述
语义解析阶段是SQL解析过程中最为复杂最有难度的一环,涉及到SQL标准,SQL优化和MapReduce等的相关理论和概念。语义分析模块在词法分析和语法分析之后执行,用于检查SQL命令是否符合语义规定,能否正确执行。
openGauss SQL parser负责语义分析的是parse_analyze函数,位于analyze.cpp中。parse_analyze会根据词法分析和语法分析得到的语法树,生成一个ParseState结构体用于记录语义分析的状态,再调用transformStmt函数,根据不同的命令类型进行相应的处理,最后生成查询树。得到的查询树在经过rewriter查询重写和optimizer查询优化处理后会交由executor查询执行模块根据查询树执行查询任务。
解析主流程parse_analyze()
函数源码及注释:
/*
* @函数功能:分析原始语法解析树,做语义分析并输出查询树。
* @入口参数:原始解析树、查询语句源文本、参数数目以及各参数的类型OID、 以及
* 两个与transformTopLevelStmt函数调用相关的bool参数。
* @出口参数:解析得到的查询树,形式是一个Query类型的结点。
*/
Query* parse_analyze(
Node* parseTree, const char* sourceText, Oid* paramTypes, int numParams, bool isFirstNode, bool isCreateView)
{
// 中间结构变量和返回结果的初始化
ParseState* pstate = make_parsestate(NULL);
Query* query = NULL;
// 断言,sourceText==NULL时抛出error信息
AssertEreport(sourceText != NULL, MOD_OPT, "para cannot be NULL");
pstate->p_sourcetext = sourceText;
if (numParams > 0) {
// parse_param.cpp中的函数,函数功能是将包含查询的引用references转化为
// 固定参数结构,即输入查询语句引用参数,为其构造FixedParamState结构
parse_fixed_parameters(pstate, paramTypes, numParams);
}
PUSH_SKIP_UNIQUE_SQL_HOOK(); // PG数据库插件hook相关
/* 将解析树转化为查询树 */
query = transformTopLevelStmt(pstate, parseTree, isFirstNode, isCreateView);
POP_SKIP_UNIQUE_SQL_HOOK(); // PG数据库插件hook相关
if (post_parse_analyze_hook && !(g_instance.status > NoShutdown)) {
(*post_parse_analyze_hook)(pstate, query);
}
pfree_ext(pstate->p_ref_hook_state);
/* 释放ParseState结构中间变量 */
free_parsestate(pstate);
// 查询树的参数信息设置
query->fixed_paramTypes = paramTypes;
query->fixed_numParams = numParams;
return query;
}
解析:在刚刚开始对SQL parser的学习时我曾经也关注过parse_analyze这个函数,但当时可以说是一脸茫然;经过前面的学习积累和对查询解析多个文件内容的学习和注解,到这时对parse_analyze函数的逻辑行为和代码内容也就不难理解了。
先来关注函数功能和入口出口参数;函数功能是对之前原始解析所得的语法解析树(Abstract Syntax Tree,AST)做相应的语义分析,最终输出处理完成后的查询树(Query Tree,QT)。 函数的入口参数包括原始解析树结构指针、查询语句源文本(主要是与参数部分相关)、参数数目以及各参数的类型OID、 以及两个与transformTopLevelStmt函数调用相关的bool变量。函数的输出即为解析得到的查询树,一个Query类型的结点(返回Query *指针)。
// 中间结构变量和返回结果的初始化
ParseState* pstate = make_parsestate(NULL);
Query* query = NULL;
函数首先声明并初始化了两个结构变量ParseState* pstate和Query* query,其中query用来保存解析所得到的查询树,作为返回结果最终返回给调用处;而pstate是语义解析过程中所必须的中间变量,它在解析过程完成后由本函数进行释放(对于ParseState和Query的结构声明后文会有讲解)。
pstate->p_sourcetext = sourceText;
if (numParams > 0) {
// parse_param.cpp中的函数,函数功能是将包含查询的引用references转化为
// 固定参数结构,即输入查询语句引用参数,为其构造FixedParamState结构
parse_fixed_parameters(pstate, paramTypes, numParams);
}
接下来函数通过断言指定了输入的参数sourceText不能是空指针,否则抛出error。然后函数调用parse_fixed_parameters函数,通过传递输入的参数numParams和paramTypes为pstate构造FixedParamState结构;关于这个函数和FixedParamState结构体在上一篇博客对parse_param.cpp的解析中有说明 SQL parser解读(11)—— parse_param.cpp解析。而之所以调用parse_fixed_parameters而不是parse_variable_parameters,是由于语法解析过程已经结束,查询语句的参数已经不能再是variable的了。
/* 将解析树转化为查询树 */
query = transformTopLevelStmt(pstate, parseTree, isFirstNode, isCreateView);
接下来函数在完成与PG数据库插件hook相关的push操作后调用transformTopLevelStmt函数对语法树进行语义解析,并将结果保存在前文生命的query变量中。至于具体的解析过程并不在本函数中实现,函数只负责完成对解析过程的调用及解析结果的处理和返回。
/* 释放ParseState结构中间变量 */
free_parsestate(pstate);
// 查询树的参数信息设置
query->fixed_paramTypes = paramTypes;
query->fixed_numParams = numParams;
// 返回查询树
return query;
解析完成后,pstate的生命周期已经到头了,函数进行对pstate的释放和内存空间回收。最后还将进行对所得查询树query的参数信息设置,完成后将query返回给调用者,至此函数结束。
transformTopLevelStmt()函数
如上文所提到的,parse_analyze通过调用transformTopLevelStmt来对语法树进行语义解析。函数transformTopLevelStmt的内部逻辑并不复杂,此处不再详述,但要注意的是,在本函数中也并未实现具体的解析过程,而是通过调用transformStmt函数对查询语句进行分类和细化,而具体的解析逻辑并不在本函数中体现。
而transformStmt函数会根据NodeTag的值,将语法树转化为不同的Stmt结构体,调用对应的语义分析函数进行处理。openGauss在语义分析阶段处理的NodeTag情况有九种,详细请参考下表。
NodeTag | 语义分析函数 | 说明 |
---|---|---|
T_InsertStmt | transformInsertStmt | 处理INSERT语句的语义 |
T_DeleteStmt | transformDeleteStmt | 处理DELETE语句的语义 |
T_UpdateStmt | transformUpdateStmt | 处理UPDATE语句的语义 |
T_MergeStmt | transformMergeStmt | 处理MERGE语句的语义 |
T_SelectStmt | transformSelectStmt、transformValuesClause、transformSetOperationStmt | 处理基本SELCET语句、SELCET VALUE语句、带有UNION、INTERSECT、EXCEPT的SELECT语句的语义 |
T_DeclareCursorStmt | transformDeclareCursorStmt | 处理DECLARE语句的语义 |
T_ExplainStmt | transformExplainStmt | 处理EXPLAIN语句的语义 |
T_CreateTableAsStmt | transformCreateTableAsStmt | 处理CREATE TABLE AS,SELECT INTO和CREATE MATERIALIZED VIEW等语句的语义 |
其他 | – | 作为UTILITY类型处理,直接在分析树上封装Query返回 |
结构体Query和ParseState
结构体Query定义在文件/src/include/nodes/parsenodes_common.h
中,ParseState的定义在文件/src/include/parser/parse_node.h
中。具体的定义细节如下(只展示部分):
/* parsenodes_common.h */
typedef struct Query {
NodeTag type;
CmdType commandType;
QuerySource querySource;
uint64 queryId;
……
bool hasAggs;
bool hasWindowFuncs;
bool hasSubLinks;
bool hasDistinctOn;
bool hasRecursive;
bool hasModifyingCTE;
bool hasForUpdate;
bool hasRowSecurity;
bool hasSynonyms;
……
List* targetList;
List* starStart;
List* starEnd;
……
Oid* fixed_paramTypes;
int fixed_numParams;
} Query;
如上文所介绍的,Query结构用于保存语义解析所得的查询树细节,并且查询树之后还会送至rewriter和optimizer(planner)进行查询重写和优化,最终交由executor执行。一条SQL语句的每个子句的语义分析结果会保存在Query的对应字段中,比如targetList存储目标属性语义分析结果,rtable存储FROM子句生成的范围表,jointree的quals字段存储WHERE子句语义分析的表达式树等等。
/* parse_node.h */
struct ParseState {
struct ParseState* parentParseState;
const char* p_sourcetext;
List* p_rtable;
List* p_joinexprs;
List* p_joinlist;
List* p_relnamespace;
List* p_varnamespace;
……
bool p_hasAggs;
bool p_hasWindowFuncs;
bool p_hasSubLinks;
bool p_hasModifyingCTE;
bool p_is_insert;
bool p_locked_from_parent;
bool p_resolve_unknowns;
bool p_hasSynonyms;
Relation p_target_relation;
RangeTblEntry* p_target_rangetblentry;
bool p_is_case_when;
……
List* p_star_start;
List* p_star_end;
List* p_star_only;
……
};
如上文所介绍的,ParseState结构用于保存语义分析的中间信息,如原始SQL命令、范围表、连接表达式、原始WINDOW子句、FOR UPDATE/FOR SHARE子句等。该结构体在语义分析入口函数parse_analyze下被初始化,在transformStmt函数下根据不同的Stmt存储不同的中间信息,完成语义分析后再被释放。
函数parse_analyze_varparams()
此函数是语义解析过程的另一个入口函数,也是函数parse_analyze的另一种应用情况。阅读函数前注释我们可以得知,当$n符号的数据类型未知,但可以通过上下文推断得知其数据类型时,调用本函数进行语义解析。函数源码如下:
Query* parse_analyze_varparams(Node* parseTree, const char* sourceText, Oid** paramTypes, int* numParams)
{
ParseState* pstate = make_parsestate(NULL);
Query* query = NULL;
AssertEreport(sourceText != NULL, MOD_OPT, "para cannot be NULL");
pstate->p_sourcetext = sourceText;
parse_variable_parameters(pstate, paramTypes, numParams);
query = transformTopLevelStmt(pstate, parseTree);
check_variable_parameters(pstate, query);
if (post_parse_analyze_hook && !(g_instance.status > NoShutdown)) {
(*post_parse_analyze_hook)(pstate, query);
}
pfree_ext(pstate->p_ref_hook_state);
free_parsestate(pstate);
return query;
}
除函数功能和应用情况外,本函数与parse_analyze函数还有两处区别:一是入口参数numParams和paramTypes分别是int * 类型和Oid ** 类型,而非int和Oid * 类型,这是因为本函数输入的参数是variable的,即应当为其构造VarParamState的数据结构而不是FixedParamState;而另一处区别就是调用的参数数据结构构造的函数为parse_variable_parameters而非parse_fixed_parameters。关于上述两种数据结构的定义和构造函数parse_fixed_parameters及parse_variable_parameters在之前对parse_param.cpp的解读过程中有所讲解SQL parser解读(11)—— parse_param.cpp解析。
函数transformStmt()
如上一篇博客 SQL parser解读(12)—— analyze.cpp解析(上)中对transformTopLevelStmt函数功能解读所述,transformTopLevelStmt会调用本函数,而本函数会根据语法树结点NodeTag的值,将语法树转化为不同的Stmt结构体,调用对应的语义分析函数进行处理。本函数仅完成上述的分流和语义分析函数的调用工作,具体的分析过程代码并未在本函数中实现。下为函数transformStmt中的部分源代码(仅展示switch-case语句块依据NodeTag分类的逻辑部分):
switch (nodeTag(parseTree)) {
case T_InsertStmt:
result = transformInsertStmt(pstate, (InsertStmt*)parseTree);
break;
case T_DeleteStmt:
result = transformDeleteStmt(pstate, (DeleteStmt*)parseTree);
break;
case T_UpdateStmt:
result = transformUpdateStmt(pstate, (UpdateStmt*)parseTree);
break;
……
case T_DeclareCursorStmt:
result = transformDeclareCursorStmt(pstate, (DeclareCursorStmt*)parseTree);
break;
case T_ExplainStmt:
result = transformExplainStmt(pstate, (ExplainStmt*)parseTree);
break;
……
default:
result = makeNode(Query);
result->commandType = CMD_UTILITY;
result->utilityStmt = (Node*)parseTree;
break;
}
语义解析中的递归过程
从前面的分析我们已经总结出,语义解析的入口函数(也是主流程函数)parse_analyze / parse_analyze_varparams会调用transformTopLevelStmt函数,而transformTopLevelStmt会调用transformStmt函数,将语法树转化为不同的Stmt结构体,调用对应的语义分析函数进行处理。
可是我们知道,transformStmt函数是根据语法树结点NodeTag的值来对语法树进行分类和转化的,也就是说,函数的每次调用只能对语法树当前结点进行分类转化,进而调用语义分析函数进行语义解析。那么问题就在于,对整棵语法树上的所有结点都进行语义分析进而得到查询树,这一过程是如何实现的呢?
static Query* transformExplainStmt(ParseState* pstate, ExplainStmt* stmt)
{
Query* result = NULL;
/* transform contained query, allowing SELECT INTO */
stmt->query = (Node*)transformTopLevelStmt(pstate, stmt->query);
/* represent the command as a utility Query */
result = makeNode(Query);
result->commandType = CMD_UTILITY;
result->utilityStmt = (Node*)stmt;
return result;
}
以其中一个语义分析函数transformExplainStmt为例,我们发现,函数重新调用了transformTopLevelStmt函数对其子结点进行解析,并且这一调用过程是在依据当前结点更新查询树之前进行的;也就是说,在对语法树的当前节点进行处理之前,通过ParseState的更新将当前结点设置为下一层的子结点,并且递归这一过程;当递归到达最底一层的结点时,自底向上地对查询树进行更新。
那么我们就已经明确了查询树自底向上更新的过程,并且可以将语义解析中函数的递归调用过程概括如下图:
函数parse_sub_analyze()
Query* parse_sub_analyze(Node* parseTree, ParseState* parentParseState, CommonTableExpr* parentCTE,bool locked_from_parent, bool resolve_unknowns)
{
ParseState* pstate = make_parsestate(parentParseState);
Query* query = NULL;
pstate->p_parent_cte = parentCTE;
pstate->p_locked_from_parent = locked_from_parent;
pstate->p_resolve_unknowns = resolve_unknowns;
if (u_sess->attr.attr_sql.td_compatible_truncation && u_sess->attr.attr_sql.sql_compatibility == C_FORMAT)
set_subquery_is_under_insert(pstate);
query = transformStmt(pstate, parseTree);
free_parsestate(pstate);
return query;
}
本函数是递归分析SQL子语句的入口函数。可以看到其大致逻辑和parse_analyze函数是相似的,其也创建一个ParseState结构并在语义解析过程完成后将其释放;不同的是本函数并不调用函数transformTopLevelStmt,而是直接调用transformStmt函数根据语法树结点NodeTag的值,将语法树转化为不同的Stmt结构体,调用对应的语义分析函数进行处理。在解析过程完成后,会返回一个Query类型指针query给调用者,此即为语义解析所得到的查询树指针。关于结构体Query和函数parse_analyze在上一篇博客中有详细的介绍 SQL parser解读(12)—— analyze.cpp解析(上)。
小结
在这篇博客中,我对/parser/analyze.cpp
文件中的函数parse_analyze_varparams、transformStmt和parse_sub_analyze进行了学习和解读,并且对parser语义解析中查询树自底向上的更新过程以及其中蕴含的函数之间的递归调用过程做了解读和概括。