Skip to content

Commit d646c92

Browse files
committed
Add doc for user-defined EXCEPTION
Also fixes the 404 issue when CN/EN switch happens.
1 parent 4c54d8e commit d646c92

15 files changed

Lines changed: 704 additions & 1 deletion

File tree

‎CN/modules/ROOT/nav.adoc‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@
3232
** xref:master/oracle_compatibility/compat_stragg.adoc[23、STRAGG 函数]
3333
** xref:master/oracle_compatibility/compat_alter_index_unusable.adoc[24、禁用索引]
3434
** xref:master/oracle_compatibility/compat_dbtimezone.adoc[25、dbtimezone]
35+
** xref:master/oracle_compatibility/compat_user_defined_exception.adoc[26、用户自定义EXCEPTION]
3536
* 容器化与云服务
3637
** 容器化指南
3738
*** xref:master/containerization/k8s_deployment.adoc[K8S部署]
@@ -112,6 +113,7 @@
112113
**** xref:master/compatibility_features_design/with_function_procedure_impl.adoc[WITH FUNCTION/PROCEDURE]
113114
**** xref:master/compatibility_features_design/create_index_online.adoc[索引 ONLINE 参数]
114115
**** xref:master/compatibility_features_design/alter_index_unusable_impl.adoc[禁用索引]
116+
**** xref:master/compatibility_features_design/user_defined_exception.adoc[用户自定义EXCEPTION]
115117
*** 内置函数
116118
**** xref:master/oracle_builtin_functions/sys_context.adoc[sys_context]
117119
**** xref:master/oracle_builtin_functions/userenv.adoc[userenv]
Lines changed: 140 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,140 @@
1+
:sectnums:
2+
:sectnumlevels: 5
3+
4+
5+
= **功能概述**
6+
7+
IvorySQL提供了兼容Oracle的用户自定义EXCEPTION功能,支持在PL/iSQL存储过程、包声明和包体中声明异常,使用RAISE按名称抛出异常,使用WHEN按名称捕获异常,并通过PRAGMA EXCEPTION_INIT将异常名称与指定错误码关联。
8+
9+
基本用法如下:
10+
11+
```sql
12+
CREATE OR REPLACE PROCEDURE test_exception IS
13+
my_exception EXCEPTION;
14+
PRAGMA EXCEPTION_INIT(my_exception, -20001);
15+
BEGIN
16+
RAISE my_exception;
17+
EXCEPTION
18+
WHEN my_exception THEN
19+
RAISE INFO 'caught: %', SQLERRM;
20+
END;
21+
/
22+
```
23+
24+
== 实现原理
25+
26+
=== 自定义EXCEPTION数据结构
27+
28+
在PLiSQL_datum_type枚举中增加PLISQL_DTYPE_EXCEPTION,用于区分用户自定义异常与普通变量、记录和包变量。
29+
30+
在pl_exception_type.h中定义PLiSQL_exception_var结构体:
31+
32+
```
33+
typedef struct PLiSQL_exception_var
34+
{
35+
PLiSQL_datum_type dtype;
36+
int dno;
37+
Oid pkgoid;
38+
char *refname;
39+
int lineno;
40+
int sqlcode;
41+
} PLiSQL_exception_var;
42+
```
43+
44+
其中dno是异常在当前PL/iSQL编译单元datum数组中的编号,refname保存异常名称,lineno保存声明位置,sqlcode保存抛出和捕获时使用的错误码。
45+
46+
plisql_build_exception函数负责创建异常datum,通过plisql_adddatum加入编译器datum数组,并以PLISQL_NSTYPE_VAR类型加入当前命名空间。命名空间类型复用普通变量类型,具体是否为异常由datum的PLISQL_DTYPE_EXCEPTION类型进一步判断。
47+
48+
没有使用PRAGMA EXCEPTION_INIT时,异常的sqlcode初始化为ERRCODE_RAISE_EXCEPTION,对应PostgreSQL内部SQLSTATE P0001。异常datum只保存编译期元数据,不保存可变的运行期值。
49+
50+
=== 自定义EXCEPTION声明语法
51+
52+
在pl_gram.y的声明语句中增加以下语法:
53+
54+
```
55+
decl_varname K_EXCEPTION ';'
56+
```
57+
58+
解析到该语法后调用plisql_build_exception创建异常,并注册到当前PL/iSQL命名空间。因此,异常名称使用已有的词法作用域和重名检查规则:内层作用域可以查找外层异常,同一作用域内不能重复声明同名对象。
59+
60+
异常是PL/iSQL的特殊标识符,不是PostgreSQL数据类型。执行器在变量初始化、datum复制、函数内存释放和包资源释放等路径中增加PLISQL_DTYPE_EXCEPTION分支。异常不需要在语句块入口初始化,也不复制运行期值;赋值、OUT参数等不允许使用异常对象的场景会拒绝该类型或跳过无关处理。
61+
62+
=== RAISE用户自定义异常
63+
64+
PLiSQL_stmt_raise结构体增加exception_var成员,用于保存编译期解析到的PLiSQL_exception_var指针。
65+
66+
编译RAISE语句时,如果词法分析器返回T_DATUM,并且datum类型为PLISQL_DTYPE_EXCEPTION,则将该异常保存到exception_var,而不是按照内置异常条件名称处理:
67+
68+
```
69+
RAISE exception_name;
70+
```
71+
72+
执行时,exec_stmt_raise从exception_var取得sqlcode和异常名称,将sqlcode写入ErrorData使用的错误码字段。没有显式指定消息时,用户自定义异常的默认错误消息为User-Defined Exception,因此异常处理器中的SQLERRM也返回该字符串。
73+
74+
错误码与错误消息分别处理。PRAGMA EXCEPTION_INIT只修改错误码,不会根据错误码自动生成ORA错误消息。可以使用RAISE的MESSAGE选项覆盖默认消息:
75+
76+
```sql
77+
RAISE my_exception USING MESSAGE = 'application error';
78+
```
79+
80+
不带异常名称和其他参数的RAISE仍用于在异常处理器中重新抛出当前异常。为避免将RAISE exception_name误判为无参数RAISE,重抛判断同时检查exception_var是否为空。
81+
82+
=== WHEN捕获用户自定义异常
83+
84+
plisql_parse_err_condition函数在解析WHEN条件时,先调用plisql_lookup_exception从当前命名空间查找用户自定义异常,再查找OTHERS和内置异常条件。
85+
86+
找到用户异常后,编译器创建PLiSQL_condition,并将PLiSQL_exception_var中的sqlcode复制到PLiSQL_condition.sqlerrstate:
87+
88+
```
89+
EXCEPTION
90+
WHEN exception_name THEN
91+
handler_statement;
92+
```
93+
94+
运行时继续复用PL/iSQL原有的异常块实现。包含EXCEPTION区域的语句块在内部子事务中执行;发生错误后回滚内部子事务,复制ErrorData,然后由exception_matches_conditions比较ErrorData.sqlerrcode和PLiSQL_condition.sqlerrstate。匹配成功后设置SQLSTATE、SQLERRM和当前错误信息,再执行对应的异常处理语句。
95+
96+
通过复用原有处理流程,用户自定义异常自动支持异常块回滚、SQLERRM、SQLSTATE以及处理器中的无参数RAISE重新抛出。
97+
98+
=== PRAGMA EXCEPTION_INIT错误码绑定
99+
100+
在关键字和语法文件中增加PRAGMA与EXCEPTION_INIT支持。语法使用any_identifier引用已经声明的异常,同时支持正整数和负整数:
101+
102+
```
103+
PRAGMA EXCEPTION_INIT(exception_name, error_code);
104+
```
105+
106+
plisql_process_pragma_exception_init在编译期完成以下处理:
107+
108+
1. 从当前命名空间查找exception_name;
109+
2. 检查对应datum是否为PLISQL_DTYPE_EXCEPTION;
110+
3. 调用plisql_validate_exception_error_code校验错误码;
111+
4. 调用plisql_exception_set_sqlcode更新异常datum中的sqlcode。
112+
113+
错误码校验规则为:正数只接受100;负数接受-1000000到-1,但拒绝-1403;同时拒绝0、除100之外的正数以及小于-1000000的数值。非法错误码在编译期报告illegal ORACLE error number错误。
114+
115+
PRAGMA EXCEPTION_INIT不生成运行期语句。后续编译RAISE时保存异常datum指针,可以取得更新后的sqlcode;后续编译WHEN时将更新后的sqlcode写入条件节点。
116+
117+
=== 存储过程与包中的异常
118+
119+
独立存储过程编译时,异常datum存储在对应PLiSQL_function的datum数组中,并遵循过程声明区的命名空间范围。
120+
121+
包编译沿用现有的包命名空间和datum管理机制。编译包体时,package_body_init恢复包声明已有的命名空间、datum和子程序信息,因此包声明中的异常可以在包体中使用,包体级异常也可以被包内子程序引用。异常是常量标识符,不需要包状态初始化,也没有需要释放的运行期值。
122+
123+
copy_plisql_datums直接共享异常datum指针;plisql_free_function_memory和包资源清理代码只识别该datum类型,不对其执行普通变量值释放。plisql_dumptree通过plisql_dump_exception输出异常名称、dno、sqlcode和声明行号,用于调试编译结果。
124+
125+
=== 构建与回归测试
126+
127+
Makefile和meson.build均加入pl_exception_type.c。Oracle回归测试列表加入plisql_exception,测试输入和预期输出分别位于:
128+
129+
```
130+
src/pl/plisql/src/sql/plisql_exception.sql
131+
src/pl/plisql/src/expected/plisql_exception.out
132+
```
133+
134+
回归测试实际执行异常抛出和捕获,并由命中的处理器向结果表写入记录。测试覆盖包级异常、过程局部异常、包内传播、多个异常、PRAGMA EXCEPTION_INIT、不同绑定错误码的处理器选择、错误码边界检查以及SQLERRM在包过程之间的传递。
135+
136+
=== 当前实现边界
137+
138+
当前实现的WHEN处理器按照sqlcode匹配,不按照异常datum的声明身份匹配。所有未使用PRAGMA EXCEPTION_INIT的用户异常共享P0001,因此两个未绑定的不同异常无法仅依靠错误码相互区分;绑定相同错误码的不同异常也具有相同的匹配结果。需要区分多个用户异常时,应通过PRAGMA EXCEPTION_INIT为它们设置不同的合法错误码。
139+
140+
PLiSQL_exception_var的sqlcode字段同时用于保存PostgreSQL内部SQLSTATE编码和PRAGMA指定的Oracle风格整数错误号。当前实现保证同一异常的RAISE与WHEN使用相同整数完成匹配,但没有提供独立的Oracle错误号到PostgreSQL SQLSTATE转换层。
Lines changed: 209 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,209 @@
1+
:sectnums:
2+
:sectnumlevels: 5
3+
4+
:imagesdir: ./_images
5+
6+
= 用户自定义EXCEPTION
7+
8+
== 目的
9+
10+
IvorySQL提供了兼容Oracle的用户自定义EXCEPTION功能,支持在PL/iSQL存储过程与包中声明用户自定义EXCEPTION。
11+
12+
本文档旨在为使用人员介绍此新增功能。
13+
14+
== 功能说明
15+
16+
IvorySQL提供的兼容Oracle的用户自定义EXCEPTION功能,包括如下内容。
17+
18+
=== 在包中声明自定义exception
19+
20+
可以在包声明部分或包体声明部分使用 `异常名 EXCEPTION;` 声明用户自定义异常。声明在包声明部分的异常可以在对应包体中使用;只在包体中声明的异常用于包体内部的过程和函数。
21+
22+
语法如下:
23+
24+
```sql
25+
exception_name EXCEPTION;
26+
```
27+
28+
自定义异常是PL/iSQL的异常对象,不是普通SQL数据类型,不能被赋值、作为表达式求值或作为过程返回值。异常名称遵循PL/iSQL的作用域规则,同一声明作用域内不能与其他变量或异常重名。
29+
30+
使用 `RAISE` 按名称抛出异常,并在 `EXCEPTION` 区域通过同一名称捕获:
31+
32+
```sql
33+
RAISE exception_name;
34+
35+
EXCEPTION
36+
WHEN exception_name THEN
37+
handler_statement;
38+
```
39+
40+
包级异常可以由包内的一个子程序抛出,再由包内调用它的另一个子程序捕获。异常向外传播时,当前语句块中的修改会按照PL/iSQL原有的异常子事务机制进行回滚,然后执行匹配的异常处理器。
41+
42+
43+
=== 在存储过程中声明exception
44+
45+
可以在独立存储过程的声明区定义局部异常。局部异常只在声明它的过程及其嵌套作用域内可见,过程外不能直接引用该异常名称。
46+
47+
```sql
48+
CREATE OR REPLACE PROCEDURE example_proc IS
49+
local_exception EXCEPTION;
50+
BEGIN
51+
RAISE local_exception;
52+
EXCEPTION
53+
WHEN local_exception THEN
54+
NULL;
55+
END;
56+
/
57+
```
58+
59+
执行 `RAISE local_exception` 时,IvorySQL创建错误并进入异常匹配流程。`WHEN local_exception` 命中后,可以在处理器中访问 `SQLERRM`。未通过 `PRAGMA EXCEPTION_INIT` 绑定错误码的用户异常使用内部SQLSTATE `P0001`;如果没有另外指定消息,`SQLERRM` 的默认内容是 `User-Defined Exception`。
60+
61+
异常处理器中使用不带参数的 `RAISE;`,可以继续向外层重新抛出当前异常。
62+
63+
64+
=== 把一个用户自定义异常名称与特定数据库错误码绑定
65+
66+
`PRAGMA EXCEPTION_INIT` 是编译期指令,用于把已经声明的用户自定义异常与指定错误码关联。它本身不是运行期语句,必须写在声明区,并且位于对应的 `EXCEPTION` 声明之后。
67+
68+
语法如下:
69+
70+
```sql
71+
exception_name EXCEPTION;
72+
PRAGMA EXCEPTION_INIT(exception_name, error_code);
73+
```
74+
75+
绑定后,执行 `RAISE exception_name` 时使用关联的错误码;`WHEN exception_name` 也根据该错误码进行匹配。这样可以用具有业务含义的名称代替数字错误码。
76+
77+
IvorySQL接受的错误码范围如下:
78+
79+
* 正数只允许 `100`,用于ANSI `NO_DATA_FOUND`;
80+
* 允许 `-1000000` 到 `-1` 之间的负整数,但不允许 `-1403`;
81+
* 不允许 `0`、除 `100` 外的正整数以及小于 `-1000000` 的整数。
82+
83+
非法错误码会在编译存储过程或包时报告 `illegal ORACLE error number ... for PRAGMA EXCEPTION_INIT`。异常名称不存在,或者名称对应的对象不是异常时,也会在编译期报错。
84+
85+
错误码和错误消息相互独立。例如,`PRAGMA EXCEPTION_INIT(my_exception, -20001)` 只把 `-20001` 绑定到异常,并不会把 `SQLERRM` 自动转换成 `ORA-20001`。使用普通 `RAISE my_exception` 且未指定消息时,`SQLERRM` 仍为 `User-Defined Exception`。如需自定义消息,可以使用:
86+
87+
```sql
88+
RAISE my_exception USING MESSAGE = 'application error';
89+
```
90+
91+
=== 使用注意事项
92+
93+
IvorySQL当前通过错误码匹配 `WHEN` 处理器,而不是通过异常对象的声明身份匹配。所有未使用 `PRAGMA EXCEPTION_INIT` 的用户异常默认使用同一个内部SQLSTATE `P0001`。如果同一异常处理区域需要区分多个用户异常,应使用 `PRAGMA EXCEPTION_INIT` 为它们绑定不同的合法错误码;绑定相同错误码的异常也无法在运行期相互区分。
94+
95+
96+
== 测试用例
97+
98+
=== 在包中声明自定义exception
99+
100+
下面的用例实际抛出并捕获包级异常。只有匹配的异常处理器得到执行,才会向结果表写入记录。
101+
102+
```
103+
CREATE TABLE plisql_exception_results
104+
(
105+
test_no NUMBER,
106+
test_name VARCHAR2(40),
107+
caught_by VARCHAR2(40),
108+
detail VARCHAR2(100)
109+
);
110+
111+
CREATE OR REPLACE PACKAGE test_exc_pkg1 IS
112+
PROCEDURE test_proc;
113+
END test_exc_pkg1;
114+
/
115+
116+
CREATE OR REPLACE PACKAGE BODY test_exc_pkg1 IS
117+
bad_interval EXCEPTION;
118+
119+
PROCEDURE test_proc IS
120+
BEGIN
121+
RAISE bad_interval;
122+
EXCEPTION
123+
WHEN bad_interval THEN
124+
INSERT INTO plisql_exception_results
125+
VALUES (1, 'package_basic', 'bad_interval', SQLERRM);
126+
END test_proc;
127+
END test_exc_pkg1;
128+
/
129+
130+
BEGIN
131+
test_exc_pkg1.test_proc();
132+
END;
133+
/
134+
```
135+
=== 在存储过程中声明exception
136+
137+
```
138+
CREATE OR REPLACE PROCEDURE test_standalone_exc IS
139+
my_exception EXCEPTION;
140+
BEGIN
141+
RAISE my_exception;
142+
EXCEPTION
143+
WHEN my_exception THEN
144+
INSERT INTO plisql_exception_results
145+
VALUES (3, 'standalone', 'my_exception', SQLERRM);
146+
END;
147+
/
148+
149+
BEGIN
150+
test_standalone_exc();
151+
END;
152+
/
153+
```
154+
155+
=== 把一个用户自定义异常名称与特定数据库错误码绑定
156+
157+
```
158+
CREATE OR REPLACE PACKAGE test_pragma_init IS
159+
PROCEDURE test_basic_pragma;
160+
END test_pragma_init;
161+
/
162+
163+
CREATE OR REPLACE PACKAGE BODY test_pragma_init IS
164+
my_exception EXCEPTION;
165+
PRAGMA EXCEPTION_INIT(my_exception, -20001);
166+
167+
PROCEDURE test_basic_pragma IS
168+
BEGIN
169+
RAISE my_exception;
170+
EXCEPTION
171+
WHEN my_exception THEN
172+
INSERT INTO plisql_exception_results
173+
VALUES (6, 'pragma_basic', 'my_exception', SQLERRM);
174+
END test_basic_pragma;
175+
END test_pragma_init;
176+
/
177+
178+
BEGIN
179+
test_pragma_init.test_basic_pragma();
180+
END;
181+
/
182+
```
183+
184+
执行以上三个用例后,可以查询实际进入的异常处理器及 `SQLERRM`:
185+
186+
```sql
187+
SELECT test_name, caught_by, detail
188+
FROM plisql_exception_results
189+
ORDER BY test_no;
190+
```
191+
192+
预期结果为:
193+
194+
```
195+
test_name | caught_by | detail
196+
-----------------+---------------+------------------------
197+
package_basic | bad_interval | User-Defined Exception
198+
standalone | my_exception | User-Defined Exception
199+
pragma_basic | my_exception | User-Defined Exception
200+
```
201+
202+
测试结束后清理对象:
203+
204+
```sql
205+
DROP PACKAGE test_exc_pkg1;
206+
DROP PROCEDURE test_standalone_exc;
207+
DROP PACKAGE test_pragma_init;
208+
DROP TABLE plisql_exception_results;
209+
```

‎EN/modules/ROOT/nav.adoc‎

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,8 @@
3131
** xref:master/oracle_compatibility/compat_create_index_online.adoc[22、ONLINE Parameter for CREATE INDEX]
3232
** xref:master/oracle_compatibility/compat_stragg.adoc[23、STRAGG function]
3333
** xref:master/oracle_compatibility/compat_alter_index_unusable_en.adoc[24、Alter Index Unusable]
34-
** xref:master/oracle_compatibility/compat_dbtimezone_en.adoc[24、dbtimezone]
34+
** xref:master/oracle_compatibility/compat_dbtimezone_en.adoc[25、dbtimezone]
35+
** xref:master/oracle_compatibility/compat_user_defined_exception_en.adoc[26、User Defined EXCEPTION]
3536
* Containerization and Cloud Service
3637
** Containerization
3738
*** xref:master/containerization/k8s_deployment.adoc[K8S deployment]
@@ -112,6 +113,7 @@
112113
*** xref:master/compatibility_features_design/with_function_procedure_impl_en.adoc[WITH FUNCTION/PROCEDURE]
113114
*** xref:master/compatibility_features_design/create_index_online.adoc[ONLINE Parameter for CREATE INDEX]
114115
*** xref:master/compatibility_features_design/alter_index_unusable_impl_en.adoc[Alter Index Unusable]
116+
*** xref:master/compatibility_features_design/user_defined_exception_en.adoc[User Defined EXCEPTION]
115117
** Built-in Functions
116118
*** xref:master/oracle_builtin_functions/sys_context.adoc[sys_context]
117119
*** xref:master/oracle_builtin_functions/userenv.adoc[userenv]

EN/modules/ROOT/pages/master/compatibility_features_design/alter_index_unusable_impl_en.adoc renamed to EN/modules/ROOT/pages/master/compatibility_features_design/alter_index_unusable_impl.adoc

File renamed without changes.

0 commit comments

Comments
 (0)