前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >Postgresql源码(77)plpgsql中参数传递和赋值

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

作者头像
mingjie
发布2022-09-26 21:52:55
6670
发布2022-09-26 21:52:55
举报

0 速查

  • call procedure(...);的入口函数ExecuteCallStmt:入参为CallStmt,函数中将CallStmt中的参数列表(可能是值、可能是表达式)赋值给fcinfo,然后通过PG函数框架进入plpgsql堆栈中,给对应入参的Dataums赋值。(下一篇介绍CallStmt中的args在哪里构造)
  • CallStmt->funccall->args:【有位置信息】【初始值】【与入参对应】
    • 灵活节点类型,数组中: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参数有值】【会填充默认值】
    • 全部是值Const类型。
    • 入参经过转换的值,会填充默认值。
    • tp12(1,2,3,e=>500)为例:
      • 共三个值。
      • 三个Const、记录1,300,500。
      • 2、3都是out不需要赋值。
  • fcinfo->args直接按funcexpr->args填充(如果是表达式就执行后在填充)
    • 从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
    • 灵活节点类型,数组中:A_Const、NamedArgExpr都有。
    • 严格与输入保持一致,不考虑out、默认值等情况。
    • tp12(1,2,3,e=>500)为例:三个A_Const记录入参1,2,3、一个NamedArgExpr记录指向参数500。尽管2、3都是out不需要赋值,这里也做记录不转换。
  • CallStmt->funcexpr->args
    • 全部是值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

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

本文参与 腾讯云自媒体分享计划,分享自作者个人站点/博客。
原始发表:2022-09-03,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 0 速查
  • 1 用例
  • 按执行顺序分析
    • 1 第一阶段:do_compile(编译不解析参数默认值)
      • 1 第二阶段:执行call tp12(1,2,3,4);
        • 进入plpgsql_exec_function
        • 上述三轮循环发生了什么
        • 赋值后e有400这个值了
        • 400是从哪来的?
      • 2 追溯:fcinfo->args来源?
        • 2.1| call tp12(1,2,3,4) | ExecuteCallStmt输入值情况
        • 2.2 | call tp12(1,2,3,e=>500) | ExecuteCallStmt输入值情况
        • 2.3 ExecuteCallStmt输入值总结
        • 2.4 使用CallStmt->funcexpr构造fcinfo->args
    • 2 实验:去除默认值位置限制
    领券
    问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档