相关
《Postgresql源码(78)plpgsql中调用call proc()时的参数传递和赋值(pl参数)》
《Postgresql源码(79)plpgsql中多层调用时参数传递关键点分析(pl参数)》
《Postgresql源码(84)语义分析——函数调用结构CallStmt的构造与函数多态的实现(pl参数)》
本文涉及模块:语法分析 、语义分析、查询重写
函数调用时在语义分析阶段,transform函数对函数入参进行分析,直观上需要完成几步工作:
- 检查是否有函数能匹配上调用输入的参数列表
- 如果匹配不上,是参数个数匹配不上,还是参数类型匹配不上?
- 如果是个数,用默认参数拼接后能否匹配?【默认参数拼接】
- 如果是类型,经过类型转换后能否匹配?【类型转换】
- 如果匹配上了多个,那么需要应该执行哪个函数?【多态】
PG对于上述问题都有了完善的处理逻辑,本篇尝试分析该过程的处理细节。
总结
总入口:transformCallStmt
【1】transformCallStmt
- 顶层函数transformCallStmt负责组装CallStmt结构(
{type=T_CallStmt,FuncCall,FuncExpr,List outargs}
) - transformCallStmt组装步骤:
- 调用ParseFuncOrColumn生成
CallStmt->FuncExpr
、生成CallStmt->FuncExpr->args
(不包含指向参数和默认参数) - 调用expand_function_arguments补充
CallStmt->FuncExpr->args
,加入指向参数和默认参数。 - 自己拼接List outargs记录输出参数
- 调用ParseFuncOrColumn生成
【2】CallStmt是如何使用的
(《Postgresql源码(79)plpgsql中多层调用时参数传递关键点分析(pl参数)》问题四:内层ExecuteCallStmt如何构造fcinfo->args?)
- 第一步:ExecuteCallStmt时遍历
CallStmt->FuncExpr->args
,把其中的值直接填入fcinfo->args[i].value
使用。 - 第二步:进入pl后,从fcinfo拿到的是紧凑的参数值数组,pl会使用传入的紧凑数组,把非out值依次赋值。
- 基于第二步推论:给pl的参数值数组必须每一个in参数都有值,多了少了都会有问题。所以顶层函数必须构造准确的参数值数组
CallStmt->FuncExpr->args
。
【3】对比Oracle
- 考虑几种情况:
- 情况一:func(入,出,默,默)
- 调用失败:call func(值):非默认参数必须全部有值,与Oracle行为一致
- 调用成功:call func(值,值)
- 调用成功:call func(值,值,值)
- 情况一:func(入,出,默,默)
- 考虑几种PG不可能发生的情况(PG要求默认参数后面必须全部是默认参数)(PG要求OUT不能有默认值)(推论:默认参数后面不能有OUT参数)
- Oracle行为:
- 情况一:func(入a,出b,默c,出d)
- 调用失败:func(值)
- 调用失败:func(值,变量)
- 调用成功:func(值,变量,d=>变量)
- 情况二:func(默a,入b)
- 调用失败:call func(值)
- 调用成功:call func(值,值)
- 调用成功:call func(b=>值)
- 情况三:func(默a,出b)
- 调用失败:call func(值)
- 调用失败:call func(值,值)
- 调用成功:call func(值,变量)
- 调用成功:call func(b=>变量)
- 情况一:func(入a,出b,默c,出d)
Oracle的IN OUT类型不能有默认参数,PG可以。
Oracle的OUT参数必须给个变量,否则执行肯定报错。
【4】PG目前的多态逻辑总结
- 第一步:ParseFuncOrColumn调用func_get_detail调用FuncnameGetCandidates
- FuncnameGetCandidates用名字找候选者
- FuncnameGetCandidates对同名候选者做参数个数检查:
- 如果 (proallargtypes个数) > (传入的全部参数个数):参数不够,需要补默认
- 如果(传入的全部参数个数+默认参数个数) < (proallargtypes个数):补上默认参数就够用了!
- 如果:(proallargtypes个数) <= (传入的全部参数个数):参数直接够用
- 如果 (proallargtypes个数) > (传入的全部参数个数):参数不够,需要补默认
- FuncnameGetCandidates对指向性参数列表调用MatchNamedCall返回argnumbers数组表示映射关系,数组严格按位置对应入参,值表示函数参数列表中应该指向的位置。在返回候选函数的参数类型数组时,会用映射关系找到正确的类型顺序记录到候选函数参数类型列表中。(没有指向型时不走MatchNamedCall且argnumbers数组为空)
- 第二步:ParseFuncOrColumn返回func_get_detail
- 【找到严格匹配候选者】遍历FuncnameGetCandidates返回结果,如果能和argtypes严格匹配,即找到best_candidate,PGPROC中拉出默认参数列表,删除掉没用的,结果放到*argdefaults返回
- 【没有严格匹配候选者】遍历FuncnameGetCandidates返回结果,没有候选者能和argtypes严格匹配
- 首先判断这是不是一个强制转换:例如 select int(3.1),如果是的可以当做强制转换返回
- 如果不是强制转换,这里肯定是参数类型对不上了,这里就开始进行【多态判断】
- 判断入参类型能不能通过转换 变成 候选者的参数类型:func_match_argtypes
- 如果只有一个候选者可以匹配, best_candidate = 当前候选者
- 如果有多个候选者经过转换可以匹配,选择一个:func_select_candidate
- 判断入参类型能不能通过转换 变成 候选者的参数类型:func_match_argtypes
1 用例
CREATE or replace PROCEDURE tp13(
a in integer,
b out integer,
c out integer,
d inout integer default 400,
e in integer default 500)
LANGUAGE plpgsql
AS $$
BEGIN
raise notice 'a: %', a;
raise notice 'b: %', b;
raise notice 'c: %', c;
raise notice 'd: %', d;
raise notice 'e: %', e;
END;
$$;
call tp13 (1,2,3,4,5);
call tp13 (1,2,3,e=>5);
2 顶层函数transformCallStmt
transformCallStmt函数负责转换所有函数调用节点,例如:
call proc1();
select func1();
transformCallStmt函数负责生成CallStmt
结构:
typedef struct CallStmt
{
NodeTag type;
FuncCall *funccall; /* from the parser */
FuncExpr *funcexpr; /* transformed call, with only input args */
List *outargs; /* transformed output-argument expressions */
} CallStmt;
CallStmt结构在之前的函数参数分析文章中反复提到过:
- 其中:FuncCall的args使用A_Const保存全部参数信息(未解析)
- 其中:FuncExpr的args使用Const只保存IN参数信息(已解析)
截取一部分:Postgresql源码(79)plpgsql中多层调用时参数传递关键点分析(pl参数)
transformCallStmt内部有两个关键调用负责生成CallStmt->FuncExpr
结构:
3 调用ParseFuncOrColumn生成FuncExpr(多态实现)
ParseFuncOrColumn
func_get_detail // 从系统表中找到函数,多态实现在这里
FuncnameGetCandidates // 第一步:找候选者
【1】用名字匹配遍历每一个结果
【2】对于某个结果,拿到PG_PROC参数类型列表proallargtypes
【3】对于某个结果,检查参数数目够不够?
【3.1】对于全指向参数或混合型参数输入
如果 (proallargtypes个数) > (传入的全部参数个数):参数不够,需要补默认
如果 (传入的全部参数个数+默认参数个数) < (proallargtypes个数):补上默认参数就够用了!
如果 (传入的全部参数个数+默认参数个数) >=(proallargtypes个数):补上默认参数也不够,不使用当前函数。
如果 (proallargtypes个数) <= (传入的全部参数个数):参数够用
MatchNamedCall判断指向参数列表是否能匹配当前函数
例如:call tp13 (1,2,3,e=>5);
tp13(a in integer, b out integer,c out integer,d inout integer default 400,e in integer default 500)
MatchNamedCall返回argnumbers数组表示映射关系:
argnumbers = [0,1,2,4,3]
给的第一个参数对应当前函数的参数列表中的0位置:a
给的第二个参数对应当前函数的参数列表中的1位置:b
给的第三个参数对应当前函数的参数列表中的2位置:c
给的第四个参数对应当前函数的参数列表中的4位置:e
只给了4个参数进来,第五个位置补充一个需要默认参数的3位置:d
【3.2】对于全非指向参数输入
只需要判断参数个数就好了,和上面逻辑类似不在赘述
func_get_detail
【找到严格匹配候选者】遍历FuncnameGetCandidates返回结果,如果能和argtypes严格匹配,即找到best_candidate
PGPROC中拉出默认参数列表,删除掉没用的,结果放到*argdefaults返回
【没有严格匹配候选者】遍历FuncnameGetCandidates返回结果,没有候选者能和argtypes严格匹配
首先判断这是不是一个强制转换:例如 select int(3.1),如果是的可以当做强制转换返回
如果不是强制转换,这里肯定是参数类型对不上了,这里就开始进行【多态判断】
判断入参类型能不能通过转换 变成 候选者的参数类型:func_match_argtypes
如果只有一个候选者可以匹配, best_candidate = 当前候选者
如果有多个候选者经过转换可以匹配,选择一个:func_select_candidate
4 混合参数位置映射关系计算MatchNamedCall
上面给出的结果
MatchNamedCall返回argnumbers数组表示映射关系:
argnumbers = [0,1,2,4,3]
给的第一个参数对应当前函数的参数列表中的0位置:a
给的第二个参数对应当前函数的参数列表中的1位置:b
给的第三个参数对应当前函数的参数列表中的2位置:c
给的第四个参数对应当前函数的参数列表中的4位置:e
只给了4个参数进来,第五个位置补充一个需要默认参数的3位置:d
涉及代码分析
执行:call tp13 (1,2,3,e=>5);
static bool
MatchNamedCall(HeapTuple proctup, int nargs, List *argnames,
bool include_out_arguments, int pronargs,
int **argnumbers)
{
入参nargs:4(由ParseFuncOrColumn在上层计算参数列表中所有元素)
入参argnames:只记录指向参数List,只有一个元素char:“e”
入参include_out_arguments:有out参数
入参pronargs:5(pg_proc记录函数需要五个参数)
Form_pg_proc procform = (Form_pg_proc) GETSTRUCT(proctup);
int numposargs = nargs - list_length(argnames);
int pronallargs;
Oid *p_argtypes;
char **p_argnames;
char *p_argmodes;
bool arggiven[FUNC_MAX_ARGS];
bool isnull;
int ap; /* call args position */
int pp; /* proargs position */
ListCell *lc;
/* Ignore this function if its proargnames is null */
(void) SysCacheGetAttr(PROCOID, proctup, Anum_pg_proc_proargnames,
&isnull);
if (isnull)
return false;
/* OK, let's extract the argument names and types */
pronallargs = get_func_arg_info(proctup,
&p_argtypes, &p_argnames, &p_argmodes);
给输出的映射关系数组申请5个int位置
/* initialize state for matching */
*argnumbers = (int *) palloc(pronargs * sizeof(int));
memset(arggiven, false, pronargs * sizeof(bool));
numposargs=nargs - list_length(argnames);
非指向参数个数 = 3 = 给了4个参数 - 有1个参数是指向型
非指向参数可以会直接记录到argnumbers数组中,同时在arggiven数组中记录已经给了的参数位置
===================================
函数要求: a
in integer, b
out integer,c
out integer,d
inout integer default 400,e
in integer default 500
调用传入: call tp13 (1,2,3,e=>5);
argnumbers:[0,1,2,x,x]
arggiven:[true,true,true,x,x]===================================
/* there are numposargs positional args before the named args */
for (ap = 0; ap < numposargs; ap++)
{
(*argnumbers)[ap] = ap;
arggiven[ap] = true;
}
检查指向参数,指向参数的位置由pp偏移
===================================
函数要求: a
in integer, b
out integer,c
out integer,d
inout integer default 400,e
in integer default 500
调用传入: call tp13 (1,2,3,e=>5);
argnumbers:[0,1,2,4,x]
arggiven:[true,true,true,false,true]===================================
/* now examine the named args */
foreach(lc, argnames)
{
char *argname = (char *) lfirst(lc);
bool found;
int i;
pp = 0;
found = false;
for (i = 0; i < pronallargs; i++)
{
/* consider only input params, except with include_out_arguments */
if (!include_out_arguments &&
p_argmodes &&
(p_argmodes[i] != FUNC_PARAM_IN &&
p_argmodes[i] != FUNC_PARAM_INOUT &&
p_argmodes[i] != FUNC_PARAM_VARIADIC))
continue;
if (p_argnames[i] && strcmp(p_argnames[i], argname) == 0)
{
指向参数的位置如果有 非指向参数了,直接弃用返回
if (arggiven[pp])
return false;
arggiven[pp] = true;
(*argnumbers)[ap] = pp;
found = true;
break;
}
/* increase pp only for considered parameters */
pp++;
}
/* if name isn't in proargnames, fail */
if (!found)
return false;
ap++;
}
开始检查默认参数够不够?
第一个需要默认参数的是first_arg_with_default=5-2=3
从numposargs开始遍历(非指向参数个数 = 3 = 给了4个参数 - 有1个参数是指向型
)
从numposargs开始是合理的,因为pos参数已经给了,不应该再去判断有没有default。
/* Check for default arguments */
if (nargs < pronargs)
{
int first_arg_with_default = pronargs - procform->pronargdefaults;
for (pp = numposargs; pp < pronargs; pp++)
{
if (arggiven[pp])
continue;
/* fail if arg not given and no default available */
如果pp的位置比第一个默认参数还要小,肯定是缺默认参数了,直接放弃返回。
例如:函数(i,i,d,d,d)
调用时(i,i)
是可以的,调用时(i)
就会报错。
if (pp < first_arg_with_default)
return false;
(*argnumbers)[ap++] = pp;
}
}
Assert(ap == pronargs); /* processed all function parameters */
return true;
}
5 调用expand_function_arguments生成FuncExpr->args
expand_function_arguments的逻辑就很简单了,只是把参数解析后拼接到FuncExpr->args中文章来源:https://www.toymoban.com/news/detail-466464.html
(其实这件事情上面的函数已经做过了,但是只是用于参数类型匹配检测,并没有真正拼接到FuncExpr->args)文章来源地址https://www.toymoban.com/news/detail-466464.html
expand_function_arguments
...
/* If so, we must apply reorder_function_arguments */
if (has_named_args)
{
args = reorder_function_arguments(args, pronargs, func_tuple);
/* Recheck argument types and add casts if needed */
recheck_cast_function_args(args, result_type,
proargtypes, pronargs,
func_tuple);
}
else if (list_length(args) < pronargs)
{
/* No named args, but we seem to be short some defaults */
args = add_function_defaults(args, pronargs, func_tuple);
/* Recheck argument types and add casts if needed */
recheck_cast_function_args(args, result_type,
proargtypes, pronargs,
func_tuple);
}
到了这里,关于Postgresql源码(84)语义分析——函数调用结构CallStmt的构造与函数多态的实现(pl参数)的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!