相關
《Postgresql源碼(78)plpgsql中調用call proc()時的參數傳遞和賦值(pl參數)》
《Postgresql源碼(79)plpgsql中多層調用時參數傳遞關鍵點分析(pl參數)》
《Postgresql源碼(84)語義分析——函數調用結構CallStmt的構造與函數多態(tài)的實現(pl參數)》
本文涉及模塊:語法分析 、語義分析、查詢重寫
函數調用時在語義分析階段,transform函數對函數入參進行分析,直觀上需要完成幾步工作:
- 檢查是否有函數能匹配上調用輸入的參數列表
- 如果匹配不上,是參數個數匹配不上,還是參數類型匹配不上?
- 如果是個數,用默認參數拼接后能否匹配?【默認參數拼接】
- 如果是類型,經過類型轉換后能否匹配?【類型轉換】
- 如果匹配上了多個,那么需要應該執(zhí)行哪個函數?【多態(tài)】
PG對于上述問題都有了完善的處理邏輯,本篇嘗試分析該過程的處理細節(jié)。
總結
總入口: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不可能發(fā)生的情況(PG要求默認參數后面必須全部是默認參數)(PG要求OUT不能有默認值)(推論:默認參數后面不能有OUT參數)
- Oracle行為:
- 情況一:func(入a,出b,默c,出d)
- 調用失敗:func(值)
- 調用失?。篺unc(值,變量)
- 調用成功:func(值,變量,d=>變量)
- 情況二:func(默a,入b)
- 調用失?。篶all func(值)
- 調用成功:call func(值,值)
- 調用成功:call func(b=>值)
- 情況三:func(默a,出b)
- 調用失?。篶all func(值)
- 調用失?。篶all func(值,值)
- 調用成功:call func(值,變量)
- 調用成功:call func(b=>變量)
- 情況一:func(入a,出b,默c,出d)
Oracle的IN OUT類型不能有默認參數,PG可以。
Oracle的OUT參數必須給個變量,否則執(zhí)行肯定報錯。
【4】PG目前的多態(tài)邏輯總結
- 第一步: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),如果是的可以當做強制轉換返回
- 如果不是強制轉換,這里肯定是參數類型對不上了,這里就開始進行【多態(tài)判斷】
- 判斷入參類型能不能通過轉換 變成 候選者的參數類型: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函數負責轉換所有函數調用節(jié)點,例如:
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(多態(tài)實現)
ParseFuncOrColumn
func_get_detail // 從系統(tǒng)表中找到函數,多態(tài)實現在這里
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),如果是的可以當做強制轉換返回
如果不是強制轉換,這里肯定是參數類型對不上了,這里就開始進行【多態(tài)判斷】
判斷入參類型能不能通過轉換 變成 候選者的參數類型: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
涉及代碼分析
執(zhí)行: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中文章來源:http://www.zghlxwxcb.cn/news/detail-466464.html
(其實這件事情上面的函數已經做過了,但是只是用于參數類型匹配檢測,并沒有真正拼接到FuncExpr->args)文章來源地址http://www.zghlxwxcb.cn/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的構造與函數多態(tài)的實現(pl參數)的文章就介紹完了。如果您還想了解更多內容,請在右上角搜索TOY模板網以前的文章或繼續(xù)瀏覽下面的相關文章,希望大家以后多多支持TOY模板網!