Skip to content

Commit 66179c5

Browse files
authored
Merge branch 'master' into sys_context
2 parents 7ffa025 + 1f73d65 commit 66179c5

14 files changed

Lines changed: 2346 additions & 121 deletions

File tree

CN/modules/ROOT/nav.adoc

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,9 @@
2626
**** xref:master/6.3.2.adoc[OUT 参数]
2727
**** xref:master/6.3.4.adoc[%TYPE、%ROWTYPE]
2828
**** xref:master/6.3.5.adoc[NLS 参数]
29+
**** xref:master/6.3.6.adoc[函数与存储过程]
30+
**** xref:master/6.3.7.adoc[嵌套子函数]
31+
**** xref:master/6.3.8.adoc[Force View]
2932
*** 内置函数
3033
**** xref:master/6.4.1.adoc[sys_context]
3134
**** xref:master/6.4.2.adoc[userenv]
@@ -48,6 +51,8 @@
4851
*** xref:master/7.15.adoc[15、OUT 参数]
4952
*** xref:master/7.16.adoc[16、%TYPE、%ROWTYPE]
5053
*** xref:master/7.17.adoc[17、NLS 参数]
54+
*** xref:master/7.18.adoc[18、Force View]
55+
*** xref:master/7.19.adoc[19、嵌套子函数]
5156
** IvorySQL贡献指南
5257
*** xref:master/8.1.adoc[社区贡献指南]
5358
*** xref:master/8.2.adoc[asciidoc语法快速参考]
Lines changed: 261 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,261 @@
1+
:sectnums:
2+
:sectnumlevels: 5
3+
4+
:imagesdir: ./_images
5+
6+
= PL/iSQL函数与存储过程
7+
8+
== 目的
9+
10+
PostgreSQL支持函数(FUNCTION)和存储过程(PROCEDURE),但是在语法上和Oracle有差异,为了让Oracle的PLSQL语句可以在IvorySQL上执行,也就是“语法兼容”,IvorySQL采取这样的解决方案:如果Oracle的子句在IvorySQL中存在相同功能的子句,则直接映射为对应的IvorySQL子句,否则只实现其语法,不实现其功能。
11+
12+
== 实现说明
13+
14+
PL/iSQL是IvorySQL中的过程语言名称,专门用来兼容Oracle的PLSQL语句。为了兼容Oracle风格的函数与存储过程语法,需要对psql客户端工具、SQL端以及PL/iSQL端做相应处理。
15+
16+
=== 客户端工具psql
17+
18+
Oracle的sqlplus工具使用斜线(/)来结束函数和存储过程,IvorySQL的客户端工具psql需要兼容同样的语法,也就是说我们常规的遇到分号发送语句给服务端的机制,当遇到Oracle风格的函数和存储过程命令时失效,改为使用斜线(/)发送命令。
19+
20+
为此在 PsqlScanStateData 结构体中增加如下字段:
21+
```
22+
bool cancel_semicolon_terminator; /* not send command when semicolon found */
23+
24+
/*
25+
* State to track boundaries of Oracle ANONYMOUS BLOCK.
26+
* Case 1: Statements starting with << ident >> is Oracle anonymous block.
27+
*/
28+
int token_count; /* # of tokens, not blank or newline since start of statement */
29+
bool anonymous_label_start; /* T if the first token is "<<" */
30+
bool anonymous_label_ident; /* T if the second token is an identifier */
31+
bool anonymous_label_end; /* T if the third token is ">>" */
32+
33+
/*
34+
* Case 2: DECLARE BEGIN ... END is Oracle anonymous block syntax.
35+
* DECLARE can also be a PostgreSQL cursor declaration statement, we need to tell this.
36+
*/
37+
bool maybe_anonymous_declare_start; /* T if the first token is DECLARE */
38+
int token_cursor_idx; /* the position of keyword CURSOR in SQL statement */
39+
40+
/*
41+
* Case 3: DECLARE BEGIN ... END is Oracle anonymous block syntax.
42+
* BEGIN can also be a PostgreSQL transaction statement.
43+
*/
44+
bool maybe_anonymous_begin_start; /* T if the first token is BEGIN */
45+
46+
```
47+
48+
同时修改 ora_psqlscan.l,添加和修改相应的词法规则, 以下是代码片段示例:
49+
```
50+
{
51+
if (is_oracle_slash(cur_state, cur_state->scanline))
52+
{
53+
/* Terminate lexing temporarily */
54+
cur_state->cancel_semicolon_terminator = false;
55+
cur_state->maybe_anonymous_declare_start = false;
56+
cur_state->maybe_anonymous_begin_start = false;
57+
cur_state->anonymous_label_start = false;
58+
cur_state->anonymous_label_ident = false;
59+
cur_state->anonymous_label_end = false;
60+
cur_state->start_state = YY_START;
61+
cur_state->token_count = 0;
62+
cur_state->token_cursor_idx = 0;
63+
cur_state->identifier_count = 0;
64+
cur_state->begin_depth = 0;
65+
cur_state->ora_plsql_expect_end_symbol = END_SYMBOL_INVALID;
66+
return LEXRES_SEMI;
67+
}
68+
ECHO;
69+
```
70+
71+
Psql工具需要检测斜线/的含义,避免将注释等部分的斜线判定为结束符,为此在oracle_fe_utils/ora_psqlscan.l文件中增加一个单独的接口is_oracle_slash来检测:
72+
```
73+
bool
74+
is_oracle_slash(PsqlScanState state, const char *line)
75+
{
76+
bool result = false;
77+
78+
switch (state->start_state)
79+
{
80+
case INITIAL:
81+
case xqs: /* treat these like INITIAL */
82+
{
83+
int len, i;
84+
bool has_slash = false;
85+
86+
len = strlen(line);
87+
for (i = 0; i < len; i++)
88+
{
89+
/* allow special char */
90+
if (line[i] == '\t' ||
91+
line[i] == '\n' ||
92+
line[i] == '\r' ||
93+
line[i] == ' ')
94+
continue;
95+
96+
if (line[i] == '/')
97+
{
98+
if (has_slash)
99+
break;
100+
has_slash = true;
101+
continue;
102+
}
103+
/* others */
104+
break;
105+
}
106+
107+
if (i == len && has_slash)
108+
result = true;
109+
}
110+
break;
111+
default:
112+
break;
113+
}
114+
115+
return result;
116+
}
117+
118+
```
119+
120+
=== SQL端
121+
122+
SQL端要能够识别函数和存储过程的创建语法,这是通过修改ora_base_yylex来实现的。这个函数预取并缓存token,如果是Oracle语法格式则组织一个SCONST发送给PLSQL端,否则从堆栈中获取之前预读的token,按照原生PG的逻辑进行处理。
123+
124+
在ora_base_yy_extra_type数据结构中增加如下字段:
125+
```
126+
/*
127+
* The native PG only cache one-token info include yylloc, yylval and token
128+
* number in yyextra, IvorySQL cache multiple tokens info using two arrays.
129+
*/
130+
int max_pushbacks; /* the max size of cache array */
131+
int loc_pushback; /* # of used tokens */
132+
int num_pushbacks; /* # of cached tokens */
133+
int *pushback_token; /* token number array */
134+
TokenAuxData *pushback_auxdata; /* auxdata array */
135+
136+
OraBodyStyle body_style;
137+
int body_start;
138+
int body_level;
139+
```
140+
141+
增加token堆栈的操作接口:
142+
|====
143+
| push_back_token
144+
| forward_token
145+
| ora_internal_yylex
146+
| internal_yylex
147+
|====
148+

149+
ora_base_yylex函数中在创建函数、过程、匿名块时会预读部分token,使用上述结构缓存到堆栈中,是为了构造一个符合Oracle PL/SQL语法的SCONST发送给PL/iSQL端去处理。具体请参考源代码。
150+
151+
152+
=== PL/iSQL端
153+
154+
该部分主要修改了pl_gram.y文件,以兼容PLSQL的函数和存储过程语法,在不影响PG原生的PL/pgSQL的前提下去兼容Oracle PL/SQL语法形式,如下是DECLARE部分兼容的代码示例,更多请参考IvorySQL源代码。
155+
156+
```
157+
/*
158+
* The declaration section of the outermost block in Oracle does not have the DECLARE keyword.
159+
*/
160+
ora_outermost_pl_block: ora_decl_sect K_BEGIN proc_sect exception_sect K_END opt_label
161+
{
162+
PLiSQL_stmt_block *new;
163+
164+
new = palloc0(sizeof(PLiSQL_stmt_block));
165+
166+
new->cmd_type = PLISQL_STMT_BLOCK;
167+
new->lineno = plisql_location_to_lineno(@2);
168+
new->stmtid = ++plisql_curr_compile->nstatements;
169+
new->label = $1.label;
170+
new->n_initvars = $1.n_initvars;
171+
new->initvarnos = $1.initvarnos;
172+
new->body = $3;
173+
new->exceptions = $4;
174+
175+
check_labels($1.label, $6, @6);
176+
plisql_ns_pop();
177+
178+
$$ = (PLiSQL_stmt *)new;
179+
}
180+
;
181+
182+
ora_decl_sect: opt_block_label opt_ora_decl_start opt_ora_decl_stmts
183+
{
184+
if ($2)
185+
{
186+
if ($1 == NULL)
187+
{
188+
plisql_ns_push(NULL, PLISQL_LABEL_BLOCK);
189+
}
190+
}
191+
}
192+
opt_ora_decl_stmts
193+
{
194+
if ($4)
195+
{
196+
plisql_IdentifierLookup = IDENTIFIER_LOOKUP_NORMAL;
197+
$$.label = ($1 == NULL ? plisql_curr_compile->namelabel : $1);
198+
if ($2 && $1 == NULL)
199+
$$.popname = true;
200+
else
201+
$$.popname = false;
202+
/* Remember variables declared in decl_stmts */
203+
$$.n_initvars = plisql_add_initdatums(&($$.initvarnos));
204+
}
205+
else
206+
{
207+
plisql_IdentifierLookup = IDENTIFIER_LOOKUP_NORMAL;
208+
$$.label = ($1 == NULL ? plisql_curr_compile->namelabel : $1);
209+
$$.n_initvars = 0;
210+
if ($2 && $1 == NULL)
211+
$$.popname = true;
212+
else
213+
$$.popname = false;
214+
$$.initvarnos = NULL;
215+
}
216+
}
217+
;
218+
219+
opt_ora_decl_start: K_DECLARE
220+
{
221+
/* Forget any variables created before block */
222+
plisql_add_initdatums(NULL);
223+
/*
224+
* Disable scanner lookup of identifiers while
225+
* we process the decl_stmts
226+
*/
227+
plisql_IdentifierLookup = IDENTIFIER_LOOKUP_DECLARE;
228+
$$ = true;
229+
}
230+
| /*EMPTY*/
231+
{
232+
/* Forget any variables created before block */
233+
plisql_add_initdatums(NULL);
234+
/*
235+
* Disable scanner lookup of identifiers while
236+
* we process the decl_stmts
237+
*/
238+
plisql_IdentifierLookup = IDENTIFIER_LOOKUP_DECLARE;
239+
$$ = false;
240+
}
241+
;
242+
243+
opt_ora_decl_stmts:
244+
ora_decl_stmts
245+
{
246+
$$ = true;
247+
}
248+
| /*EMPTY*/
249+
{
250+
$$ = false;
251+
}
252+
253+
ora_decl_stmts: ora_decl_stmts ora_decl_stmt
254+
| ora_decl_stmt
255+
;
256+
257+
ora_decl_stmt: decl_statement
258+
;
259+
260+
```
261+
Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
1+
:sectnums:
2+
:sectnumlevels: 5
3+
4+
:imagesdir: ./_images
5+
6+
= 嵌套子函数
7+
8+
== 目的
9+
10+
- 嵌套子函数:定义在函数、存储过程或匿名块内部的函数或存储过程,也称为 subproc 或 inner 函数。
11+
- 父函数:承载嵌套函数的外层函数、存储过程或匿名块,执行过程中负责实际触发子函数调用。
12+
13+
== 实现说明
14+
15+
=== 嵌套子函数语法识别
16+
17+
==== 识别嵌套写法
18+
19+
当 `DECLARE` 块里出现 `function ... is/as begin ... end` 结构时,`pl_gram.y` 会调用 `plisql_build_subproc_function()`(对应创建普通函数,这一阶段相当于在 `pg_proc` 中更新 catalog 中的注册信息):
20+
21+
. 在 `PLiSQL_function` 的 `subprocfuncs[]` 数组中创建 `PLiSQL_subproc_function` 结构,记录名称、参数、返回类型、属性等,获得一个下标 `fno` 作为该子函数的标识。
22+
. 调用 `plisql_check_subprocfunc_properties()` 校验声明与定义的合法组合。
23+
24+
==== 数据项保存
25+
26+
保存到父函数的 `datum` 表:编译期的 `PLiSQL_function->datums` 描述子函数里的变量、记录字段,`PLiSQL_execstate->datums` 保存着执行过程中的变量。
27+
28+
==== 保存多态函数模板
29+
30+
如果子函数里使用了多态参数,在语法阶段保存到 `subprocfunc->src`,同时将 `has_poly_argument` 设成 `true`,执行时按不同实参类型重新编译。
31+
32+
=== 父函数重新编译
33+
34+
. 父函数的 `PLiSQL_function` 结构多了一个 `subprocfuncs` 数组,里面每个元素就是刚才创建的 `PLiSQL_subproc_function`。
35+
. 子函数结构体 `PLiSQL_subproc_function` 有一个哈希表指针 `HTAB *poly_tab`,默认为空。当子函数里使用了多态函数时,`has_poly_argument` 为 `true`,则会在初次编译时初始化 `poly_tab`。`poly_tab` 的 key 是 `PLiSQL_func_hashkey`,记录着子函数的 `fno`、输入参数类型等; value 是编译好的 `PLiSQL_function *`(`plisql` 函数的执行上下文)。
36+
37+
=== 调用时解析
38+
39+
. 编译过程中,`pg` 解析器会生成一个 `ParseState` 结构,`plisql_subprocfunc_ref()` 会通过 `ParseState->p_subprocfunc_hook()` 找到父函数的 `PLiSQL_function`,调用 `plisql_ns_lookup()` 找到所有同名子函数的 `fno`,根据参数个数、类型找到最合适的多态函数。
40+
. `FuncExpr` 结构构造时会对子函数进行标记,方便后期执行阶段识别:`function_from = FUNC_FROM_SUBPROCFUNC`,`parent_func` 指向父级 `PLiSQL_function`,`funcid = fno`。
41+
. `plisql_call_handler()` 当 `function_from == FUNC_FROM_SUBPROCFUNC`,会用 `parent_func + fno` 找到对应的 `PLiSQL_subproc_function`:
42+
.. 如果不是多态:直接复用 `subprocfunc->function` 里的动作树。
43+
.. 如果是多态:先在 `poly_tab` 查有没有编译结果;没有就调用 `plisql_dynamic_compile_subproc()` 编译,放进 `poly_tab` 缓存。
44+
. 子函数开始执行之前,`plisql_init_subprocfunc_globalvar()` 会把父函数的 `datum` 表中有关的变量 fork 一份,这样子函数可以获取到父函数的变量值,也不会污染父函数的变量空间;执行后由 `plisql_assign_out_subprocfunc_globalvar()` 把需要回写的变量更新到父函数的 `datum` 表。
45+
46+
== 模块设计
47+
48+
=== PL/iSQL 语法扩展
49+
50+
- `pl_gram.y` 新增子过程声明、嵌套定义的产生式,并在创建过程中记录 `lastoutvardno`、子过程信息等元数据。
51+
- 支持在子函数内引用父过程变量、子过程以及自定义类型。
52+
53+
当 DECLARE 块内出现 `function ... is/as begin ... end` 结构时,`pl_gram.y` 会调用 `plisql_build_subproc_function()` 进行编译:
54+
55+
. 在 `PLiSQL_function` 的 `subprocfuncs[]` 中创建 `PLiSQL_subproc_function` 结构,记录名称、参数、返回类型和属性,分配下标 `fno` 作为子函数标识。
56+
. 调用 `plisql_check_subprocfunc_properties()` 校验声明与定义属性组合是否合法,防止重复或缺失声明造成的语义错误。
57+
58+
=== 数据项保存
59+
60+
父函数的 Datum 表在编译期和执行期分别缓存子函数能访问的变量:
61+
62+
. `PLiSQL_function->datums` 保存子函数编译阶段可见的变量与记录字段信息。
63+
. `PLiSQL_execstate->datums` 在执行阶段持有实时的变量数值,实现运行期访问。
64+
65+
=== 多态函数模板
66+
67+
若子函数包含多态参数,语法阶段会:
68+
69+
. 将子函数源文本拷贝到 `subprocfunc->src`。
70+
. 设置 `has_poly_argument = true`,为后续按实参类型动态编译做好准备。
71+
72+
=== 父函数重新编译
73+
74+
- 父函数的 `PLiSQL_function` 结构新增 `subprocfuncs` 数组,每个元素对应一个 `PLiSQL_subproc_function`。
75+
- `PLiSQL_subproc_function` 持有 `HTAB *poly_tab` 指针;当 `has_poly_argument` 为 `true` 时,在首次编译时初始化该缓存,键为 `PLiSQL_func_hashkey`(子函数 `fno` + 实参类型),值为编译后的 `PLiSQL_function`。
76+
77+
=== 解析器钩子
78+
79+
编译期间 PostgreSQL 解析器会构造 `ParseState`,`plisql_subprocfunc_ref()` 通过 `ParseState->p_subprocfunc_hook()` 连接父函数,调用 `plisql_ns_lookup()` 找到同名子函数的全部 `fno`,并依据参数个数与类型挑选最佳候选,实现重载分发。
80+
81+
=== FuncExpr 标记
82+
83+
构造 `FuncExpr` 时会标记嵌套调用信息,便于执行阶段识别:
84+
85+
- `function_from = FUNC_FROM_SUBPROCFUNC`。
86+
- `parent_func` 指向父级 `PLiSQL_function`。
87+
- `funcid = fno`,用于快速定位子函数定义。
88+
89+
=== 嵌套函数查找机制
90+
91+
- `plisql_subprocfunc_ref()` 作为 `ParseState->p_subprocfunc_hook` 实现入口,复用名称空间查询逻辑。
92+
- `plisql_get_subprocfunc_detail()` 依据参数数量、类型与命名匹配规则挑选最优候选,是嵌套函数重载的关键。
93+
94+
=== 执行路径
95+
96+
. `plisql_call_handler()` 判断 `function_from` 后,通过 `parent_func + fno` 找到目标 `PLiSQL_subproc_function`。
97+
. 对普通子函数,直接复用 `subprocfunc->function` 缓存;
98+
. 对多态子函数,先查询 `poly_tab`,未命中时调用 `plisql_dynamic_compile_subproc()` 动态编译并写入缓存。
99+
100+
=== 变量同步
101+
102+
- `plisql_init_subprocfunc_globalvar()` 在子函数执行前拷贝父函数 Datum 表中的相关变量,保证子函数读取到外层最新状态。
103+
- `plisql_assign_out_subprocfunc_globalvar()` 在返回前回写 OUT/INOUT 变量,确保父子函数数据一致性且互不污染。
104+
105+
=== PSQL 端语句发送
106+
107+
- `psqlscan.l` 调整 `proc_func_define_level` 和 `begin_depth` 的入栈/出栈逻辑,确保嵌套函数体整体发送至 SQL 端。
108+
- 只有当嵌套层级回到 0 且遇到分号时,才触发发送,避免子函数块被拆分。
109+
110+
=== SQL 层返回值获取
111+
112+
- 普通函数通过 `funcid` 访问 `pg_proc`;嵌套函数依赖 `FuncExpr.parent_func` 承载的 `PLiSQL_function`。
113+
- 为此实现一组函数指针(`plisql_register_internal_func()` 注册)供 SQL 层回调,按需获取嵌套函数名称、返回类型与 OUT 参数信息。

0 commit comments

Comments
 (0)