引言
本篇主要对SQL语法解析相关文件parse_oper.cpp的内容进行解析。
文件路径
src/common/backend/parser/parse_oper.cpp
主要结构体声明
文件首先定义了两个在操作符处理过程中会用到的结构体OprCacheEntry和OprCacheKey。
typedef struct OprCacheKey {
char oprname[NAMEDATALEN]; // operator名称
Oid left_arg; /* Left input OID, or 0 if prefix op */
// 左侧操作符的OID,如果这是一个前缀操作符则此项为0.
Oid right_arg; /* Right input OID, or 0 if postfix op */
// 右侧操作符的OID,如果这是一个后缀操作符则此项为0.
Oid search_path[MAX_CACHED_PATH_LEN]; // 搜索路径
bool use_a_style_coercion;
} OprCacheKey;
typedef struct OprCacheEntry {
/* the hash lookup key MUST BE FIRST */
OprCacheKey key; // 哈希查找值必须是第一项
/* OID of the resolved operator */
Oid opr_oid; // 解决后的OID
} OprCacheEntry;
它们的包含关系和数据成员概括如下。
首先来看OprCacheKey。它有五个数据成员,分别是操作符的名称oprname,左右操作符的OID值,搜索路径(数据类型为一个OID数组)以及一个bool类型变量use_a_style_coercion。对于各个成员变量所司职责我们目前还不能准确知晓,在对文件中其他部分内容进行解析后也许会有更进一步的理解。
另外显然结构类型OID是Operator ID的意思,即每个操作符与其唯一的OID值相对应。这也体现在OprCacheEnrty的结构中:每一个OprCacheKey值唯一地对应一个OID值opr_oid。
LookupOperName()
函数源代码及注释如下:
/*
* LookupOperName
* Given a possibly-qualified operator name and exact input datatypes,
* look up the operator.
* 给定一个可能存在的操作符名称以及确切的字节信息,查找对应操作符的OID
*
* Pass oprleft = InvalidOid for a prefix op, oprright = InvalidOid for
* a postfix op.
* 当为一个前缀操作符时,入口参数oprleft = InvalidOid
* 当为一个后缀操作符时,入口参数oprright = InvalidOid
*
* If the operator is not found, we return InvalidOid if noError is true,
* else raise an error. pstate and location are used only to report the
* error position; pass NULL/-1 if not available.
* 如果查找失败,且noError的值为true,返回InvalidOid,否则报出异常。
* 入口参数pstate和location仅用于向调用者报告异常发生的位置。
*/
Oid LookupOperName(ParseState* pstate, List* opername, Oid oprleft, Oid oprright, bool noError, int location)
{
Oid result;
// 根据OprCacheKey.left_arg.oprname,OprCacheKey.left_arg, OprCacheKey.right_arg
// 的值查找OID,保存在result中;查找成功则返回result。
result = OpernameGetOprid(opername, oprleft, oprright);
if (OidIsValid(result))
return result;
/* we don't use op_error here because only an exact match is wanted */
// OpernameGetOprid查找失败,即result = InValid,并且输入信号noError = false
// 这表示调用者允许将查找失败的情况当作error处理。此if块用于报告错误信息。
if (!noError) {
char oprkind;
if (!OidIsValid(oprleft))
oprkind = 'l';
else if (!OidIsValid(oprright))
oprkind = 'r';
else
oprkind = 'b';
ereport(ERROR,
(errcode(ERRCODE_UNDEFINED_FUNCTION),
errmsg("operator does not exist: %s", op_signature_string(opername, oprkind, oprleft, oprright)),
parser_errposition(pstate, location)));
}
return InvalidOid;
}
函数功能:给定一个可能存在的操作符名称以及确切的字段信息,查找其对应的OID。
入口参数:操作符名称opername,操作符字段中left_arg和right_arg的值,用以指定查找失败时是否抛出error的bool变量noError,以及与确定错误信息相关的变量。
出口参数:查找结果。若查找成功返回对应的OID,否则返回InvalidOid。
函数的内部逻辑是十分简单的,通过调用OpernameGetOprid函数,传入操作符名称opername、左右操作符OID oprleft、oprright进行操作符OID的查找。如果查找成功,返回得到的OID值;否则返回InvalidOid。至于具体的查找逻辑不在本函数中实现,函数只负责调用查找函数并将查找结果返回给上级。
另外,函数通过指定入口参数noError来选择当查找OID失败时是否要报出错误信息。当noError = false时,如果操作符对应的OID查找失败,则函数会负责错误位置的定位和错误信息的生成,并将错误报出。
LookupOperNameTypeNames()
函数源码及注释如下:
/*
* LookupOperNameTypeNames
* Like LookupOperName, but the argument types are specified by
* TypeName nodes.
* 与函数LookupOperName相似,但当传入oprleft,oprright的类型
* 为TypeName*时调用本函数。
*/
Oid LookupOperNameTypeNames(
ParseState* pstate, List* opername, TypeName* oprleft, TypeName* oprright, bool noError, int location)
{
Oid leftoid, rightoid;
if (oprleft == NULL)
leftoid = InvalidOid;
else
leftoid = typenameTypeId(pstate, oprleft);
if (oprright == NULL)
rightoid = InvalidOid;
else
rightoid = typenameTypeId(pstate, oprright);
return LookupOperName(pstate, opername, leftoid, rightoid, noError, location);
}
函数的功能与LookupOperName函数类似,即给定操作符的名称opername以及相应的字段信息,对其对应的OID进行查找。不同的是,本函数传入的oprleft,oprright的数据类型是TypeName*。函数先通过调用typenameTypeId函数完成oprleft和oprright对应的OID信息的查找,获取其对应的OID后调用LookupOperName函数,获取操作符对应的OID信息。
函数的入口参数、出口参数除oprleft、oprright数据类型不同外其他均与LookupOperName函数相同。事实上函数是对LookupOperName函数的扩展。函数内部并不实现具体的查找逻辑,只负责函数的调用,并将查找结果返回给上级。另外,由于本函数调用了LookupOperName函数,因此也有可能raise error(即noError=false且查找失败的情况)。
可哈希hashable
先来补充一个概念——hashable可哈希的。通常来讲,一个对象在其生命周期内,如果保持不变,就是hashable(可哈希的)。可哈希的数据类型,通常为不可变的数据结构,如字符串、对象集等等;同理,可变的数据结构如数组(列表)、集合等,就是不可哈希的(unhashable)。
哈希是一个将大体量数据映射为很小的数据的过程,以便我们可以在固定的时间复杂度下进行对应数据的查询(很多时候时间复杂度仅为O(1))。因此,哈希以及可哈希的概念对高效的算法和数据结构是十分重要的。
函数get_sort_group_operators()
函数源码及注释如下:
/*
* get_sort_group_operators - get default sorting/grouping operators for type
* 为指定类型获取其与比较和分组相关的操作符
*
* We fetch the "<", "=", and ">" operators all at once to reduce lookup
* overhead (knowing that most callers will be interested in at least two).
* However, a given datatype might have only an "=" operator, if it is
* hashable but not sortable. (Other combinations of present and missing
* operators shouldn't happen, unless the system catalogs are messed up.)
* 我们一次完成对 "<", "=", ">"的查找以避免反复调用造成的开销(因为大多数
* 时候我们都需要知悉以上至少两种的操作符)。尽管如此,一种给定的数据类型
* 是可能只支持 "=" 这一个比较操作的,如果他是不可比较但是可哈希的。
*
* If an operator is missing and the corresponding needXX flag is true,
* throw a standard error message, else return InvalidOid.
* 如果以上的一种操作符是缺失的,但传入的信号neadXX指定了这个操作符
* 是必须的,那么会抛出一个标准错误信息;否则返回InvalidOid。
*
* In addition to the operator OIDs themselves, this function can identify
* whether the "=" operator is hashable.
* 作为对每种类型对应比较操作符的OID的补充,函数还可以识别 "=" 是否是可哈希的。
*
* Callers can pass NULL pointers for any results they don't care to get.
* 如果某个比较符的OID是不需要的,调用者可以对相应的参数传入NULL。
*
* Note: the results are guaranteed to be exact or binary-compatible matches,
* since most callers are not prepared to cope with adding any run-time type
* coercion steps.
* 注意:结果是保证精确或二进制兼容的匹配,因为大多数调用者还没有准备好应对
* 添加任何运行时类型强制措施。
*/
void get_sort_group_operators(
Oid argtype, bool needLT, bool needEQ, bool needGT, Oid* ltOpr, Oid* eqOpr, Oid* gtOpr, bool* isHashable)
{
TypeCacheEntry* typentry = NULL;
int cache_flags;
Oid lt_opr; // 操作符对应的 "<" 的OID
Oid eq_opr; // "=" 的OID
Oid gt_opr; // ">" 的OID
bool hashable = false;
/*
* Look up the operators using the type cache.
* 利用数据类型cache进行符号的查找
*
* Note: the search algorithm used by typcache.c ensures that the results
* are consistent, ie all from matching opclasses.
*/
if (isHashable != NULL)
cache_flags = TYPECACHE_LT_OPR | TYPECACHE_EQ_OPR | TYPECACHE_GT_OPR | TYPECACHE_HASH_PROC;
else
cache_flags = TYPECACHE_LT_OPR | TYPECACHE_EQ_OPR | TYPECACHE_GT_OPR;
/* 通过传入的argtype获取type对应的TypeCacheEntry */
typentry = lookup_type_cache(argtype, cache_flags);
lt_opr = typentry->lt_opr; // <
eq_opr = typentry->eq_opr; // =
gt_opr = typentry->gt_opr; // >
hashable = OidIsValid(typentry->hash_proc);
/* Report errors if needed */
// 可选的报错信息
if ((needLT && !OidIsValid(lt_opr)) || (needGT && !OidIsValid(gt_opr)))
ereport(ERROR,
(errcode(ERRCODE_UNDEFINED_FUNCTION),
errmsg("could not identify an ordering operator for type %s", format_type_be(argtype)),
errhint("Use an explicit ordering operator or modify the query.")));
if (needEQ && !OidIsValid(eq_opr))
ereport(ERROR,
(errcode(ERRCODE_UNDEFINED_FUNCTION),
errmsg("could not identify an equality operator for type %s", format_type_be(argtype))));
/* Return results as needed */
// 将结果保存在指针中返回
if (ltOpr != NULL)
*ltOpr = lt_opr;
if (eqOpr != NULL)
*eqOpr = eq_opr;
if (gtOpr != NULL)
*gtOpr = gt_opr;
if (isHashable != NULL)
*isHashable = hashable;
}
函数功能:为指定的类型Type获取其相关的用于比较/分组的操作符信息(<,=,>)。
入口参数:操作符类型OID argtype以及指定三个比较符是否必须的bool信号needGT,needEQ,needLT。
出口参数:指定类型下<、=、>的OID信息,以及可哈希布尔值hashable(均以指针方式传递)。
对于不同的操作符,其对应的比较符号<,=,>的OID是不同的,并非是相同的OID。函数传入某操作符的类型,通过类型cache查找上述三个比较符的OID。查找得到的结果储存在如下数据结构中:
/* --- typcache.h --- */
typedef struct TypeCacheEntry {
/* typeId is the hash lookup key and MUST BE FIRST */
Oid type_id; /* OID of the data type */
int16 typlen;
//……
Oid eq_opr; /* the equality operator */
Oid lt_opr; /* the less-than operator */
Oid gt_opr; /* the greater-than operator */
//……
} TypeCacheEntry;
可以看到结构TypeCacheEntry包含了十余个字段信息(部分省略),其中包括操作符类型type_id,类型的字节长度,以及其对应的 >,<,= 三种比较符的OID值。
通过调用lookup_type_cache函数进行cache查找后,访问TypeCacheEntry对应的字段信息就得到了三个操作符对应的OID。此外,TypeCacheEntry中还包括hash_proc字段,这表示该操作符是否是hashable的。
在得到三个比较符号的OID及hashable信息后,要根据调用者输入的neadXX信号来进行是否抛出error的选择。例如,指定neadGT = true,即大于号”>”的OID是必需的,若此时从TypeCacheEntry结构中读取到的gt_opr字段为InValid,则要生成对应的错误信息,并且raise error。
最终,函数将查找的三个比较符的OID信息及hashable信息存放在调用者指定的存储空间中,返回调用处(此情况为error不发生的前提下)。
make_op()
函数源码及注释(部分源代码省略):
/*
* make_op
* Operator expression construction.
* 为操作符operator构造expression结构。
*
* Transform operator expression ensuring type compatibility.
* This is where some type conversion happens.
* 操作符expression的转换要确保类型兼容,在这里进行一些类型转换。
*/
Expr* make_op(ParseState* pstate, List* opname, Node* ltree, Node* rtree, int location, bool inNumeric)
{
/* 输入操作符名称及左右操作符的结构树,为其构造expression结构 */
Oid ltypeId, rtypeId; // 左右操作符的OID
Operator tup; // 构造OpExpr之前需先获得操作符对应的Operator结构
Form_pg_operator opform; // 与Pg_Operator和Operator的类型转换相关
Oid actual_arg_types[2]; // 由ltree,rtree决定的实际左右操作符的OID
Oid declared_arg_types[2]; // 在Pg_Operator结构下的OID
int nargs; // 左右操作符的数目(0,1,2)。
List* args = NIL; // 代表左右操作符的树结构
Oid rettype; // 返回操作符类型的OID
OpExpr* result = NULL; // 返回的expression结构
// 通过exprType函数计算左右操作符的OID,
// 再利用oper(),right_oper(),left_oper()函数获取操作符对应的Operator结构
if (rtree == NULL) {
/* right operator */
ltypeId = exprType(ltree);
rtypeId = InvalidOid;
tup = right_oper(pstate, opname, ltypeId, false, location);
} else if (ltree == NULL) {
/* left operator */
rtypeId = exprType(rtree);
ltypeId = InvalidOid;
tup = left_oper(pstate, opname, rtypeId, false, location);
} else {
/* otherwise, binary operator */
ltypeId = exprType(ltree);
rtypeId = exprType(rtree);
……
tup = oper(pstate, opname, ltypeId, rtypeId, false, location, inNumeric);
}
// 与Pg_Operator与Operator之间的类型转换有关。opform是一个Form_pg_operator类型的结构体变量
opform = check_operator_is_shell(opname, pstate, location, tup);
/* Do typecasting and build the expression tree */
// 进行类型转换,构造表达式树结构
if (rtree == NULL) {
/* right operator */
args = list_make1(ltree);
actual_arg_types[0] = ltypeId; // 实际的OID,由ltree决定的ltypeId
declared_arg_types[0] = opform->oprleft; // PG中的OID
nargs = 1;
} else if (ltree == NULL) {
/* left operator */
args = list_make1(rtree);
actual_arg_types[0] = rtypeId; // 实际的OID,由rtree决定的rtypeId
declared_arg_types[0] = opform->oprright; // PG中的OID
nargs = 1;
} else {
/* otherwise, binary operator */
args = list_make2(ltree, rtree);
actual_arg_types[0] = ltypeId;
actual_arg_types[1] = rtypeId;
declared_arg_types[0] = opform->oprleft;
declared_arg_types[1] = opform->oprright;
nargs = 2; // 左右操作符均是valid的
}
……
/* perform the necessary typecasting of arguments */
// 为局部变量进行必要的类型转换
make_fn_arguments(pstate, args, actual_arg_types, declared_arg_types);
/* and build the expression node */
// 完善返回的expression结构的内容
result = makeNode(OpExpr);
result->opno = oprid(tup); // 操作符类型OID
result->opfuncid = opform->oprcode; // PG中的OID
……
/* opcollid and inputcollid will be set by parse_collate.c */
result->args = args; // 左右操作符的树结构
result->location = location;
ReleaseSysCache(tup); // 释放内存空间
return (Expr*)result; // 转化为Expr*类型返回
}
函数功能:输入操作符名称及左右操作符的语法树结构,为其创建一个OpExpr结构并返回给调用者。
入口参数:操作符名、左右操作符的语法树ltree,rtree、与查询语句语义解析相关的指针pstate等。
出口参数:构造的OpExpr结构,强制类型转换为Expr* 后返回给调用者。
函数为操作符构造OpExpr结构,首先需要确定其Operator具体结构。输入参数中包括左右操作符的语法树ltree和rtree,函数先通过调用exprType函数得到语法树对应的OID的值,即ltypeId和rtypeId;再通过ltypeId和rtypeId的值以及操作符名称opname对操作符对应的Operator结构进行cache查找。
查找过程是通过调用函数oper、left_oper和right_oper来实现的,本函数内部并不实现具体的查找逻辑。函数oper、left_oper和right_oper同样在parse_oper.cpp中,它们的作用是给定操作符名opername以及左右操作符的OID值,为操作符查找其对应的Operator结构信息(分别对应左右操作符语法树都存在,右操作符InValid和左操作符InValid三种情况)。
这样就得到了操作符对应的Operator结构。接下来通过调用check_operator_is_shell函数得到一个Form_pg_operator类型的结构体变量 opform,它是与pg_operator和Operator之间的类型转换相关的。
接下来为操作符构建树形的语法结构。具体操作是,通过函数list_make1和list_make2(分别对应左右操作符有1或2个有效的情况)获取左右操作符语法树的List* 结构,存放在变量 List* arg中。并利用数组actual_arg_types来保存在之前的过程中计算得到的ltypeId和rtypeId的值,declared_arg_types保存opform字段oprleft和oprright的值,再调用make_fn_arguments函数,为actual_arg_types和declared_arg_types做必要的类型转换。
接下来调用makeNode函数,指定node类型为OpExpr,就得到了一个初始的OpExpr结点,令OpExpr类型指针result指向它。然后通过result来访问结点的各个数据域,为其赋值,如操作符OID、语法树列表指针等。完成上述操作后,释放cache空间,并将result强制转换成Expr* 类型(由命名推测为OpExpr父类)后返回给上级。
总结
在本篇博客中,我进行了对SQL语法分析中操作符处理相关内容的探索,并对 parse_oper.cpp 文件中的部分函数进行了学习和注解工作。