前文 《Postgresql源码(41)plpgsql函数编译执行流程分析》 《Postgresql源码(46)plpgsql中的变量类型及对应关系》 《Postgresql源码(49)plpgsql函数编译执行流程分析总结》
以一个带简单赋值、出入参、变量有默认值的普通函数为例,分析执行过程。触发器等其他函数的执行过程大同小异,核心流程基本不变,就是多了几个默认工具变量。相比《Postgresql源码(46)plpgsql中的变量类型及对应关系》这篇总结更清晰简单。
例子:
CREATE OR REPLACE FUNCTION sn(x int, y int, OUT sum int, OUT prod int) AS $$
DECLARE
a integer DEFAULT 32;
b CONSTANT integer := 10;
BEGIN
sum := x + y + 1;
prod := x * y * b;
raise notice '(%, %)', sum, prod;
END;
$$ LANGUAGE plpgsql;
select sn(2, 3);
全部放过发到server这里解析主要是发现语句什么时候结束)、server的gram.y的语法解析(函数代码整理包装放到pg_proc系统表里面),在pl中要经历两大步骤:编译、执行
上面是整体流程的直观认识,下面做一些细节分析
总结:系统表拿到源码;初始化命名空间ns_top、变量空间datums;函数参数、返回值构造进入ns_top、datums;调用yacc解析语法树,并构造语法块list;所有信息拷贝到function结构体中;function记录到htab中;编译完成。
do_compile触发器的编译流程会有所差异,这里只分析普通函数的编译过程:
// 所有信息存入function,then add it to function hash table
do_compile
...
// 拿到源码
prosrcdatum = SysCacheGetAttr(PROCOID, procTup,Anum_pg_proc_prosrc, &isnull)
proc_source = TextDatumGetCString(prosrcdatum)
...
// 【初始化语法解析】
plpgsql_scanner_init(proc_source)
...
// 【初始化命名空间】
plpgsql_ns_init
// {itemtype = PLPGSQL_NSTYPE_LABEL, itemno = 0, prev = 0x0, name = 0x1c992c0 "sn"}
plpgsql_ns_push(NameStr(procStruct->proname), PLPGSQL_LABEL_BLOCK)
plpgsql_start_datums
...
switch (function->fn_is_trigger)
case PLPGSQL_NOT_TRIGGER
...
get_func_arg_info
// 【处理参数】
for (i = 0; i < numargs; i++)
...
plpgsql_build_variable
...
// 处理结束
// 函数采纳数: (x int, y int, OUT sum int, OUT prod int)
// ns_top值: prod->$4->sum->$3->y->$2->x->$1->sn
// ns_top类型: | ---- PLPGSQL_NSTYPE_VAR ---- | ---- PLPGSQL_NSTYPE_LABEL ---- |
// datums有4个:
// {dtype = PLPGSQL_DTYPE_VAR, dno = 0, refname = 0x1c99360 "x" ...
// {dtype = PLPGSQL_DTYPE_VAR, dno = 1, refname = 0x1c99468 "y" ...
// {dtype = PLPGSQL_DTYPE_VAR, dno = 2, refname = 0x1c99608 "sum" ...
// {dtype = PLPGSQL_DTYPE_VAR, dno = 3, refname = 0x1c997a8 "prod" ...
// 如果多于一个Out,创建一个PLpgSQL_row类型组装所有返回值,所以datum在增加一个row
if (num_out_args > 1 || (num_out_args == 1 && function->fn_prokind == PROKIND_PROCEDURE))
...
// {dtype = PLPGSQL_DTYPE_ROW, dno = 4, refname = 0x7f3266610ea8 "(unnamed row)" ...
plpgsql_adddatum
...
// 【构造返回值】
// 增加一个found
// {itemtype = PLPGSQL_NSTYPE_VAR, itemno = 5, prev = 0x1c99800, name = 0x1c999f0 "found"}
// {dtype = PLPGSQL_DTYPE_VAR, dno = 5, refname = 0x1c999c0 "found" ...
plpgsql_build_variable
// 【开始语法解析】plpgsql_yyparse下一章展开
parse_rc = plpgsql_yyparse()
// 语法解析的所有语法块都串在plpgsql_parse_result上
function->action = plpgsql_parse_result
plpgsql_scanner_finish
// 没有return,给语法块list增加一个dummy return
add_dummy_return(function);
// plpgsql_Datums 拷贝到 function->datums
plpgsql_finish_datums
// 所有信息存入hash
plpgsql_HashTableInsert(function, hashkey);
// 编译完成
return function;
总结:
组装运行状态estate;拷贝变量datums;func->fn_argvarnos找到入参在datums中的位置然后入参赋值;
然后进入exec_stmt_block:
1、初始化当前语法块的所有变量(使用block的n_initvars、initvarnos找到datums变量,然后赋值即可)。如果变量有默认值,使用exec_assign_expr把默认值当做SQL执行出结果,赋值给变量。
2、当前块有没有异常处理,没有的话直接执行;有的话需要走try/cache流程(使用block的body部分);
3、开始遍历body链表的第一个元素,赋值。这里的值都是使用PLpgSQL_expr表示的,因为值可以是一个语句
其他:
plpgsql_exec_function
...
// 组装estate
plpgsql_estate_setup
...
// 变量信息拷贝:func->datums到estate->datums
copy_plpgsql_datums
// 入参赋值
for (i = 0; i < func->fn_nargs; i++)
// 拿到入参位置
int n = func->fn_argvarnos[i]
switch (estate.datums[n]->dtype)
// 普通变量
case PLPGSQL_DTYPE_VAR:
PLpgSQL_var *var = (PLpgSQL_var *) estate.datums[n]
assign_simple_var(&estate, var, fcinfo->args[i].value, fcinfo->args[i].isnull, false)
// 赋值后 x=2 y=3
// var = {dtype = PLPGSQL_DTYPE_VAR, dno = 0, refname = 0x1c99360 "x",
// ... value = 2, isnull = false, freeval = false, promise = PLPGSQL_PROMISE_NONE}
// 给found赋值false
exec_set_found(&estate, false)
// 开始执行
rc = exec_toplevel_block(&estate, func->action)
exec_stmt_block(estate, block)
// 【第一步】初始化当前语法块的所有变量(使用block的n_initvars、initvarnos找到datums变量,然后赋值即可)
for (i = 0; i < block->n_initvars; i++)
int n = block->initvarnos[i];
PLpgSQL_datum *datum = estate->datums[n]
// 有默认值需要赋值
if (var->default_val == NULL)
...
else
// 【第一步】变量有默认值,使用exec_assign_expr把默认值当做SQL执行出结果,赋值给变量
// var->default_val是一个expr,expr相当于一个SQL语句,需要调用SQL引擎执行一遍
exec_assign_expr(estate, (PLpgSQL_datum *) var, var->default_val)
// 第一次跑生成执行计划
exec_prepare_plan
// 执行SQL
value = exec_eval_expr
// 赋值
exec_assign_value
...
// 【第二步】当前块有没有异常处理,没有的话直接执行;有的话需要走try/cache流程;(使用block的body部分)
if (block->exceptions)
...
else
// 【第二步】调用exec_stmts开始执行语法块。body应该是一个4个元素的list,包含三句函数体中写的赋值和一句后加的return
rc = exec_stmts(estate, block->body);
// stmts == block->body,开始遍历
foreach(s, stmts)
switch (stmt->cmd_type)
PLPGSQL_STMT_ASSIGN
// 【第三步】开始遍历body链表的第一个元素,赋值
rc = exec_stmt_assign(estate, (PLpgSQL_stmt_assign *) stmt)
// 【第三步】这里的值都是使用PLpgSQL_expr表示的,因为值可以是一个语句。
// 这样看这个函数就比较好理解了,第一个参数是运行时变量;第二个是变量;第三个是值。
exec_assign_expr(estate, estate->datums[stmt->varno], stmt->expr)
...
return PLPGSQL_RC_OK;