Postgresql源码(77)plpgsql中参数传递和赋值(pl参数)

2022-11-30 16:12:10 浏览数 (1)

相关 《Postgresql源码(77)plpgsql中参数传递和赋值(pl参数)》 《Postgresql源码(78)plpgsql中调用call proc()时的参数传递和赋值(pl参数)》 《Postgresql源码(79)plpgsql中多层调用时参数传递关键点分析(pl参数)》

0 速查

  • call procedure(...);的入口函数ExecuteCallStmt:入参为CallStmt,函数中将CallStmt中的参数列表(可能是值、可能是表达式)赋值给fcinfo,然后通过PG函数框架进入plpgsql堆栈中,给对应入参的Dataums赋值。(下一篇介绍CallStmt中的args在哪里构造)
  • 入参有两组args,一组是语法解析直接生成的funccall;一组是经过优化器的funcexpr:
  • CallStmt->funccall->args:【有位置信息】【初始值】【与入参对应】
代码语言:txt复制
- 灵活节点类型,数组中:A_Const、NamedArgExpr都有。
- 严格与输入保持一致,不考虑out、默认值等情况。
- tp12(1,2,3,e=>500)为例:      
    - 共四个值。
    - 三个A_Const记录入参1,2,3。
    - 一个NamedArgExpr记录指向参数500。
    - 尽管2、3都是out不需要赋值,这里也做记录不转换。CallStmt->funcexpr->args:【无位置信息】【加工值】【只有in、inout参数有值】【会填充默认值】
代码语言:txt复制
- 全部是值Const类型。
- 入参经过转换的值,会填充默认值。
- tp12(1,2,3,e=>500)为例,函数定义见下面用例:      
    - 共三个值。
    - 三个Const、记录1,300,500。300来自默认值,500来自e的定向赋值。
    - 2、3都是out类型参数不需要赋值,直接舍弃。fcinfo->args直接按funcexpr->args填充(如果是表达式就执行后在填充)
代码语言:txt复制
- 从0位置开始紧凑填充。

1 用例

代码语言:javascript复制
CREATE or replace PROCEDURE tp12(
  a in integer, 
  b out integer,
  c out integer,
  d inout integer default 300,
  e in integer default 400)
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 tp12(1,2,3,4,5);
NOTICE:  a: 1
NOTICE:  b: <NULL>
NOTICE:  c: <NULL>
NOTICE:  d: 4
NOTICE:  e: 5
 b | c 
--- ---
   |  
==============================================
call tp12(1,2,3,4);
NOTICE:  a: 1
NOTICE:  b: <NULL>
NOTICE:  c: <NULL>
NOTICE:  d: 4
NOTICE:  e: 400
 b | c 
--- ---
   |  
==============================================
call tp12(1,2,3);
NOTICE:  a: 1
NOTICE:  b: <NULL>
NOTICE:  c: <NULL>
NOTICE:  d: 300
NOTICE:  e: 400
 b | c 
--- ---
   |  
==============================================
call tp12(1,2);
ERROR:  procedure tp12(integer, integer) does not exist
==============================================
call tp12(1,2,3,e=>500);
NOTICE:  a: 1
NOTICE:  b: <NULL>
NOTICE:  c: <NULL>
NOTICE:  d: 300
NOTICE:  e: 500
 b | c 
--- ---
   |  
==============================================

按执行顺序分析

1 第一阶段:do_compile(编译不解析参数默认值)

编译总结:

  • 从编译结果看,编译不处理default默认值。
  • 编译生成的datums数组中不区分in、out,没有参数信息。
  • 参数信息在functions->fn_argvarnos、functions->out_param_varno中记录。
代码语言:javascript复制
CREATE or replace PROCEDURE tp12(
  a in integer,                ---> function->fn_argvarnos
  b out integer,               ---> function->out_param_varno
  c out integer,               ---> function->out_param_varno
  d inout integer default 300, ---> function->fn_argvarnos   --->function->out_param_varno
  e in integer default 400)    ---> function->fn_argvarnos
  • (复习)PLpgSQL_row用于保存多变量结果:
    • select into后面的变量记录在row中。
    • for loop targetlist记录在row中。
    • 游标的参数列表记录在row中。

编译完成变量:

代码语言:javascript复制
plpgsql_nDatums = 7

p *((PLpgSQL_var*)plpgsql_Datums[0])
{
  dtype = PLPGSQL_DTYPE_VAR, dno = 0, refname = 0x29d8260 "a", lineno = 0, 
  isconst = false, notnull = false, default_val = 0x0, datatype = 0x29d81e8,
  cursor_explicit_expr = 0x0, cursor_explicit_argrow = 0, cursor_options = 0, 
  value = 0, isnull = true, freeval = false, promise = PLPGSQL_PROMISE_NONE
}

p *((PLpgSQL_var*)plpgsql_Datums[1])
{
  dtype = PLPGSQL_DTYPE_VAR, dno = 1, refname = 0x29d8368 "b", lineno = 0, 
  isconst = false, notnull = false, default_val = 0x0, datatype = 0x29d82f0,
  cursor_explicit_expr = 0x0, cursor_explicit_argrow = 0, cursor_options = 0, 
  value = 0, isnull = true, freeval = false, promise = PLPGSQL_PROMISE_NONE
}

p *((PLpgSQL_var*)plpgsql_Datums[2])
{
  dtype = PLPGSQL_DTYPE_VAR, dno = 2, refname = 0x29d8508 "c", lineno = 0, 
  isconst = false, notnull = false, default_val = 0x0, datatype = 0x29d83f8,
  cursor_explicit_expr = 0x0, cursor_explicit_argrow = 0, cursor_options = 0, 
  value = 0, isnull = true, freeval = false, promise = PLPGSQL_PROMISE_NONE
}

p *((PLpgSQL_var*)plpgsql_Datums[3])
{
  dtype = PLPGSQL_DTYPE_VAR, dno = 3, refname = 0x29d86a8 "d", lineno = 0, 
  isconst = false, notnull = false, default_val = 0x0, datatype = 0x29d8598,
  cursor_explicit_expr = 0x0, cursor_explicit_argrow = 0, cursor_options = 0, 
  value = 0, isnull = true, freeval = false, promise = PLPGSQL_PROMISE_NONE
}

p *((PLpgSQL_var*)plpgsql_Datums[4])
{
  dtype = PLPGSQL_DTYPE_VAR, dno = 4, refname = 0x29d8848 "e", lineno = 0, 
  isconst = false, notnull = false, default_val = 0x0, datatype = 0x29d8738,
  cursor_explicit_expr = 0x0, cursor_explicit_argrow = 0, cursor_options = 0, 
  value = 0, isnull = true, freeval = false, promise = PLPGSQL_PROMISE_NONE
}

p *((PLpgSQL_row*)plpgsql_Datums[5])
{
  dtype = PLPGSQL_DTYPE_ROW, dno = 5, refname = 0x7f02d092bea8 "(unnamed row)", 
  lineno = -1, isconst = false, notnull = false, default_val = 0x0,
  rowtupdesc = 0x29d7830, nfields = 3, fieldnames = 0x29d8930, varnos = 0x29d7c90
}

p *((PLpgSQL_var*)plpgsql_Datums[6])
{
  dtype = PLPGSQL_DTYPE_VAR, dno = 6, refname = 0x29d8a78 "found", lineno = 0, 
  isconst = false, notnull = false, default_val = 0x0, datatype = 0x29d8968, 
  cursor_explicit_expr = 0x0, cursor_explicit_argrow = 0, cursor_options = 0, 
  value = 0, isnull = true, freeval = false,promise = PLPGSQL_PROMISE_NONE
}

编译完成和参数相关的三个关键变量:

  • In参数分别记录在Datums数组中
  • Out参数不管有几个,拼装到一个row中用function->out_param_varno指向dno(变量数组位置)
代码语言:javascript复制
本地临时变量             | --->     保存到编译结果function中
-----------------------|------------------------------
procStruct->pronargs   | --->     function->fn_nargs        = 3
in_arg_varnos          | --->     function->fn_argvarnos    = {0 ,3, 4}    整形数组:存所有in参数变量的dno
out_arg_variables      | --->     function->out_param_varno = 5            整形:只存一个dno指向row,row保存所有out参数

1 第二阶段:执行call tp12(1,2,3,4);

总结:函数入参赋值是遍历datums中需要值的变量,然后按顺序拿fcinfo->args数组的值。

比如下面case:

代码语言:javascript复制
for i(次数 = 编译完了需要入参的数量 = 3)
  编译完Datums中0、3、4行需要入参
  循环三次用0、1、2从fcinfo->args[i]数组中拿三个赋值,数组中一定有值(没传的话外面会把默认值填进来)

进入plpgsql_exec_function

代码语言:javascript复制
plpgsql_exec_function
  for (i = 0; i < func->fn_nargs; i  )   // 循环3个入参
    int n = func->fn_argvarnos[i];       // 拿dno
    switch (estate.datums[n]->dtype)
      case PLPGSQL_DTYPE_VAR:
        xxx
      case PLPGSQL_DTYPE_REC:
        xxx

上述三轮循环发生了什么

给a、d、e赋值:

代码语言:javascript复制
n = 0     a in integer                 从fcinfo->args[0].value拿到1通过assign_simple_var赋值给var
          b out integer
          c out integer
n = 3     d inout integer default 300  从fcinfo->args[1].value拿到4通过assign_simple_var赋值给var
n = 4     e in integer default 400     从fcinfo->args[2].value拿到400通过assign_simple_var赋值给var

赋值后e有400这个值了

代码语言:javascript复制
p *((PLpgSQL_var*)estate->datums[4])
{
  dtype = PLPGSQL_DTYPE_VAR, dno = 4, refname = 0x29dc868 "e", lineno = 0, 
  isconst = false, notnull = false, default_val = 0x0, datatype = 0x29dc758,
  cursor_explicit_expr = 0x0, cursor_explicit_argrow = 0, cursor_options = 0, 
  value = 400, isnull = false, freeval = false, promise = PLPGSQL_PROMISE_NONE
}

400是从哪来的?

到这里发现函数的默认值400是从fcinfo->args[2].value拿出来的,那么后面继续追溯下这个值是哪里添加的:

2 追溯:fcinfo->args来源?

函数调用堆栈:fcinfo的值是在ExecuteCallStmt中构造的:

代码语言:javascript复制
standard_ProcessUtility
  ExecuteCallStmt
    LOCAL_FCINFO(fcinfo, FUNC_MAX_ARGS) <----------入参默认的400在这里就填好了,存在fcinfo里
    ...
    /* 在这里把传入的参数值赋值 */
    foreach(lc, fexpr->args)
      exprstate = ExecPrepareExpr(lfirst(lc), estate);
      val = ExecEvalExprSwitchContext(exprstate, econtext, &isnull);
      fcinfo->args[i].value = val;
      fcinfo->args[i].isnull = isnull;
    /* 结束 */
    ...
    FunctionCallInvoke
      plpgsql_call_handler
        SPI_connect_ext
        plpgsql_compile
        PG_TRY
          plpgsql_exec_function
        PG_FINALLY
        PG_END_TRY
        SPI_finish

2.1| call tp12(1,2,3,4) | ExecuteCallStmt输入值情况

2.2 | call tp12(1,2,3,e=>500) | ExecuteCallStmt输入值情况

CallStmt->funccall->args

代码语言:javascript复制
  {type = T_A_Const, val = {type = T_Integer, val = {ival = 1}}, location = 10}
  {type = T_A_Const, val = {type = T_Integer, val = {ival = 2}}, location = 12}
  {type = T_A_Const, val = {type = T_Integer, val = {ival = 3}}, location = 14}
  {xpr = {type = T_NamedArgExpr}, arg = 0x28dd4d0, name = 0x28dd0a8 "e", argnumber = 4, location = 16}
    NamedArgExpr->arg: {xpr = {type = T_Const}, consttype = 23, consttypmod = -1, constcollid = 0, constlen = 4, constvalue = 500, constisnull = false, constbyval = true, location = 19}

CallStmt->funcexpr->args

代码语言:javascript复制
{xpr = {type = T_Const}, consttype = 23, consttypmod = -1, constcollid = 0, constlen = 4, constvalue = 1, constisnull = false, constbyval = true, location = 10}
{xpr = {type = T_Const}, consttype = 23, consttypmod = -1, constcollid = 0, constlen = 4, constvalue = 300, constisnull = false, constbyval = true, location = -1}
{xpr = {type = T_Const}, consttype = 23, consttypmod = -1, constcollid = 0, constlen = 4, constvalue = 500, constisnull = false, constbyval = true, location = 19}

2.3 ExecuteCallStmt输入值总结

  • CallStmt->funccall->args
代码语言:txt复制
- 灵活节点类型,数组中:A_Const、NamedArgExpr都有。
- 严格与输入保持一致,不考虑out、默认值等情况。
- tp12(1,2,3,e=>500)为例:三个A_Const记录入参1,2,3、一个NamedArgExpr记录指向参数500。尽管2、3都是out不需要赋值,这里也做记录不转换。CallStmt->funcexpr->args
代码语言:txt复制
- 全部是值Const类型
- 入参经过转换的值,会填充默认值
- tp12(1,2,3,e=>500)为例:三个Const、记录1,300,500;因为2、3都是out不需要赋值。

2.4 使用CallStmt->funcexpr构造fcinfo->args

fcinfo->args完整继承CallStmt->funcexpr的值,从0开始紧凑填充。

代码语言:javascript复制
ExecuteCallStmtCallStmt *stmt, ParamListInfo params, bool atomic, DestReceiver *dest)
 	...
	fexpr = stmt->funcexpr;
  	...
  	i = 0;
	foreach(lc, fexpr->args)
	{
		ExprState  *exprstate;
		Datum		val;
		bool		isnull;

		exprstate = ExecPrepareExpr(lfirst(lc), estate);

		val = ExecEvalExprSwitchContext(exprstate, econtext, &isnull);

		fcinfo->args[i].value = val;
		fcinfo->args[i].isnull = isnull;

		i  ;
	}

2 实验:去除默认值位置限制

PG要求默认值参数后面的参数,必须也有默认值,去除这个限制会有一些问题。

例如:

代码语言:javascript复制
CREATE or replace PROCEDURE tp13(
  a in integer, 
  b out integer,
  c out integer,
  d inout integer default 300,
  e in integer)
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);

默认值错误的给e了,这条本应该报错,因为e无值。

代码语言:javascript复制
postgres=# call tp13(1,2,3,4);
NOTICE:  a: 1
NOTICE:  b: <NULL>
NOTICE:  c: <NULL>
NOTICE:  d: 4
NOTICE:  e: 300
 b | c | d 
--- --- ---
   |   | 4
(1 row)

报错:call tp13(1,2,3, e=>5);

直接报错了,这条应该正常执行,d给默认值300,e给传入值5。

代码语言:javascript复制
postgres=# call tp13(1,2,3, e=>5);

ERROR:  procedure tp13(integer, integer, integer, e => integer) does not exist
LINE 1: call tp13(1,2,3, e=>5);
             ^
HINT:  No procedure matches the given name and argument types. You might need to add explicit type casts.

这里的问题发生在ParseFuncOrColumn中:

代码语言:javascript复制
#0  errfinish (filename=0xc50838 "parse_func.c", lineno=620, funcname=0xc51e80 <__func__.22568> "ParseFuncOrColumn") at elog.c:515
#1  0x000000000064470f in ParseFuncOrColumn (pstate=0x1660268, funcname=0x165ff60, fargs=0x16603d8, last_srf=0x0, fn=0x1660148, proc_call=true, location=5)
    at parse_func.c:620
#2  0x00000000005fb84b in transformCallStmt (pstate=0x1660268, stmt=0x16601a0) at analyze.c:2971
#3  0x00000000005f5c17 in transformStmt (pstate=0x1660268, parseTree=0x16601a0) at analyze.c:357
#4  0x00000000005f5a5f in transformOptionalSelectInto (pstate=0x1660268, parseTree=0x16601a0) at analyze.c:268
#5  0x00000000005f591a in transformTopLevelStmt (pstate=0x1660268, parseTree=0x16601d8) at analyze.c:218
#6  0x00000000005f56f4 in parse_analyze (parseTree=0x16601d8, sourceText=0x165f4e0 "call tp13(1,2,3, e=>5);", paramTypes=0x0, numParams=0, queryEnv=0x0)
    at analyze.c:127
#7  0x0000000000975d70 in pg_analyze_and_rewrite (parsetree=0x16601d8, query_string=0x165f4e0 "call tp13(1,2,3, e=>5);", paramTypes=0x0, numParams=0, 
    queryEnv=0x0) at postgres.c:656
#8  0x00000000009764a1 in exec_simple_query (query_string=0x165f4e0 "call tp13(1,2,3, e=>5);") at postgres.c:1129
#9  0x000000000097ab59 in PostgresMain (argc=1, argv=0x7ffd124d16f0, dbname=0x1688cb0 "postgres", username=0x1688c88 "mingjiegao") at postgres.c:4494
#10 0x00000000008b6d4e in BackendRun (port=0x1680870) at postmaster.c:4530
#11 0x00000000008b66cd in BackendStartup (port=0x1680870) at postmaster.c:4252
#12 0x00000000008b2b45 in ServerLoop () at postmaster.c:1745
#13 0x00000000008b2417 in PostmasterMain (argc=1, argv=0x16590d0) at postmaster.c:1417
#14 0x00000000007b4c93 in main (argc=1, argv=0x16590d0) at main.c:209

下面一篇展开分析这个问题。

0 人点赞