From d646c92ed2ef34e4eace71b3e4e3f63a97d98a27 Mon Sep 17 00:00:00 2001 From: Steven Niu Date: Mon, 21 Sep 2026 09:11:57 +0000 Subject: [PATCH 1/2] Add doc for user-defined EXCEPTION Also fixes the 404 issue when CN/EN switch happens. --- CN/modules/ROOT/nav.adoc | 2 + .../user_defined_exception.adoc | 140 ++++++++++++ .../compat_user_defined_exception.adoc | 209 +++++++++++++++++ EN/modules/ROOT/nav.adoc | 4 +- ...en.adoc => alter_index_unusable_impl.adoc} | 0 .../user_defined_exception.adoc | 140 ++++++++++++ ...adoc => with_function_procedure_impl.adoc} | 0 .../{pg_readonly_en.adoc => pg_readonly.adoc} | 0 .../{zhparser_en.adoc => zhparser.adoc} | 0 ...zone_impl_en.adoc => dbtimezone_impl.adoc} | 0 .../{vsize_en.adoc => vsize.adoc} | 0 ....adoc => compat_alter_index_unusable.adoc} | 0 ...imezone_en.adoc => compat_dbtimezone.adoc} | 0 .../compat_user_defined_exception.adoc | 210 ++++++++++++++++++ ...e_en.adoc => with_function_procedure.adoc} | 0 15 files changed, 704 insertions(+), 1 deletion(-) create mode 100644 CN/modules/ROOT/pages/master/compatibility_features_design/user_defined_exception.adoc create mode 100644 CN/modules/ROOT/pages/master/oracle_compatibility/compat_user_defined_exception.adoc rename EN/modules/ROOT/pages/master/compatibility_features_design/{alter_index_unusable_impl_en.adoc => alter_index_unusable_impl.adoc} (100%) create mode 100644 EN/modules/ROOT/pages/master/compatibility_features_design/user_defined_exception.adoc rename EN/modules/ROOT/pages/master/compatibility_features_design/{with_function_procedure_impl_en.adoc => with_function_procedure_impl.adoc} (100%) rename EN/modules/ROOT/pages/master/ecosystem_components/{pg_readonly_en.adoc => pg_readonly.adoc} (100%) rename EN/modules/ROOT/pages/master/ecosystem_components/{zhparser_en.adoc => zhparser.adoc} (100%) rename EN/modules/ROOT/pages/master/oracle_builtin_functions/{dbtimezone_impl_en.adoc => dbtimezone_impl.adoc} (100%) rename EN/modules/ROOT/pages/master/oracle_builtin_functions/{vsize_en.adoc => vsize.adoc} (100%) rename EN/modules/ROOT/pages/master/oracle_compatibility/{compat_alter_index_unusable_en.adoc => compat_alter_index_unusable.adoc} (100%) rename EN/modules/ROOT/pages/master/oracle_compatibility/{compat_dbtimezone_en.adoc => compat_dbtimezone.adoc} (100%) create mode 100644 EN/modules/ROOT/pages/master/oracle_compatibility/compat_user_defined_exception.adoc rename EN/modules/ROOT/pages/master/oracle_compatibility/{with_function_procedure_en.adoc => with_function_procedure.adoc} (100%) diff --git a/CN/modules/ROOT/nav.adoc b/CN/modules/ROOT/nav.adoc index 7ed31aff..4f2d33fa 100644 --- a/CN/modules/ROOT/nav.adoc +++ b/CN/modules/ROOT/nav.adoc @@ -32,6 +32,7 @@ ** xref:master/oracle_compatibility/compat_stragg.adoc[23、STRAGG 函数] ** xref:master/oracle_compatibility/compat_alter_index_unusable.adoc[24、禁用索引] ** xref:master/oracle_compatibility/compat_dbtimezone.adoc[25、dbtimezone] +** xref:master/oracle_compatibility/compat_user_defined_exception.adoc[26、用户自定义EXCEPTION] * 容器化与云服务 ** 容器化指南 *** xref:master/containerization/k8s_deployment.adoc[K8S部署] @@ -112,6 +113,7 @@ **** xref:master/compatibility_features_design/with_function_procedure_impl.adoc[WITH FUNCTION/PROCEDURE] **** xref:master/compatibility_features_design/create_index_online.adoc[索引 ONLINE 参数] **** xref:master/compatibility_features_design/alter_index_unusable_impl.adoc[禁用索引] +**** xref:master/compatibility_features_design/user_defined_exception.adoc[用户自定义EXCEPTION] *** 内置函数 **** xref:master/oracle_builtin_functions/sys_context.adoc[sys_context] **** xref:master/oracle_builtin_functions/userenv.adoc[userenv] diff --git a/CN/modules/ROOT/pages/master/compatibility_features_design/user_defined_exception.adoc b/CN/modules/ROOT/pages/master/compatibility_features_design/user_defined_exception.adoc new file mode 100644 index 00000000..0e715b33 --- /dev/null +++ b/CN/modules/ROOT/pages/master/compatibility_features_design/user_defined_exception.adoc @@ -0,0 +1,140 @@ +:sectnums: +:sectnumlevels: 5 + + += **功能概述** + +IvorySQL提供了兼容Oracle的用户自定义EXCEPTION功能,支持在PL/iSQL存储过程、包声明和包体中声明异常,使用RAISE按名称抛出异常,使用WHEN按名称捕获异常,并通过PRAGMA EXCEPTION_INIT将异常名称与指定错误码关联。 + +基本用法如下: + +```sql +CREATE OR REPLACE PROCEDURE test_exception IS + my_exception EXCEPTION; + PRAGMA EXCEPTION_INIT(my_exception, -20001); +BEGIN + RAISE my_exception; +EXCEPTION + WHEN my_exception THEN + RAISE INFO 'caught: %', SQLERRM; +END; +/ +``` + +== 实现原理 + +=== 自定义EXCEPTION数据结构 + +在PLiSQL_datum_type枚举中增加PLISQL_DTYPE_EXCEPTION,用于区分用户自定义异常与普通变量、记录和包变量。 + +在pl_exception_type.h中定义PLiSQL_exception_var结构体: + +``` +typedef struct PLiSQL_exception_var +{ + PLiSQL_datum_type dtype; + int dno; + Oid pkgoid; + char *refname; + int lineno; + int sqlcode; +} PLiSQL_exception_var; +``` + +其中dno是异常在当前PL/iSQL编译单元datum数组中的编号,refname保存异常名称,lineno保存声明位置,sqlcode保存抛出和捕获时使用的错误码。 + +plisql_build_exception函数负责创建异常datum,通过plisql_adddatum加入编译器datum数组,并以PLISQL_NSTYPE_VAR类型加入当前命名空间。命名空间类型复用普通变量类型,具体是否为异常由datum的PLISQL_DTYPE_EXCEPTION类型进一步判断。 + +没有使用PRAGMA EXCEPTION_INIT时,异常的sqlcode初始化为ERRCODE_RAISE_EXCEPTION,对应PostgreSQL内部SQLSTATE P0001。异常datum只保存编译期元数据,不保存可变的运行期值。 + +=== 自定义EXCEPTION声明语法 + +在pl_gram.y的声明语句中增加以下语法: + +``` +decl_varname K_EXCEPTION ';' +``` + +解析到该语法后调用plisql_build_exception创建异常,并注册到当前PL/iSQL命名空间。因此,异常名称使用已有的词法作用域和重名检查规则:内层作用域可以查找外层异常,同一作用域内不能重复声明同名对象。 + +异常是PL/iSQL的特殊标识符,不是PostgreSQL数据类型。执行器在变量初始化、datum复制、函数内存释放和包资源释放等路径中增加PLISQL_DTYPE_EXCEPTION分支。异常不需要在语句块入口初始化,也不复制运行期值;赋值、OUT参数等不允许使用异常对象的场景会拒绝该类型或跳过无关处理。 + +=== RAISE用户自定义异常 + +PLiSQL_stmt_raise结构体增加exception_var成员,用于保存编译期解析到的PLiSQL_exception_var指针。 + +编译RAISE语句时,如果词法分析器返回T_DATUM,并且datum类型为PLISQL_DTYPE_EXCEPTION,则将该异常保存到exception_var,而不是按照内置异常条件名称处理: + +``` +RAISE exception_name; +``` + +执行时,exec_stmt_raise从exception_var取得sqlcode和异常名称,将sqlcode写入ErrorData使用的错误码字段。没有显式指定消息时,用户自定义异常的默认错误消息为User-Defined Exception,因此异常处理器中的SQLERRM也返回该字符串。 + +错误码与错误消息分别处理。PRAGMA EXCEPTION_INIT只修改错误码,不会根据错误码自动生成ORA错误消息。可以使用RAISE的MESSAGE选项覆盖默认消息: + +```sql +RAISE my_exception USING MESSAGE = 'application error'; +``` + +不带异常名称和其他参数的RAISE仍用于在异常处理器中重新抛出当前异常。为避免将RAISE exception_name误判为无参数RAISE,重抛判断同时检查exception_var是否为空。 + +=== WHEN捕获用户自定义异常 + +plisql_parse_err_condition函数在解析WHEN条件时,先调用plisql_lookup_exception从当前命名空间查找用户自定义异常,再查找OTHERS和内置异常条件。 + +找到用户异常后,编译器创建PLiSQL_condition,并将PLiSQL_exception_var中的sqlcode复制到PLiSQL_condition.sqlerrstate: + +``` +EXCEPTION + WHEN exception_name THEN + handler_statement; +``` + +运行时继续复用PL/iSQL原有的异常块实现。包含EXCEPTION区域的语句块在内部子事务中执行;发生错误后回滚内部子事务,复制ErrorData,然后由exception_matches_conditions比较ErrorData.sqlerrcode和PLiSQL_condition.sqlerrstate。匹配成功后设置SQLSTATE、SQLERRM和当前错误信息,再执行对应的异常处理语句。 + +通过复用原有处理流程,用户自定义异常自动支持异常块回滚、SQLERRM、SQLSTATE以及处理器中的无参数RAISE重新抛出。 + +=== PRAGMA EXCEPTION_INIT错误码绑定 + +在关键字和语法文件中增加PRAGMA与EXCEPTION_INIT支持。语法使用any_identifier引用已经声明的异常,同时支持正整数和负整数: + +``` +PRAGMA EXCEPTION_INIT(exception_name, error_code); +``` + +plisql_process_pragma_exception_init在编译期完成以下处理: + +1. 从当前命名空间查找exception_name; +2. 检查对应datum是否为PLISQL_DTYPE_EXCEPTION; +3. 调用plisql_validate_exception_error_code校验错误码; +4. 调用plisql_exception_set_sqlcode更新异常datum中的sqlcode。 + +错误码校验规则为:正数只接受100;负数接受-1000000到-1,但拒绝-1403;同时拒绝0、除100之外的正数以及小于-1000000的数值。非法错误码在编译期报告illegal ORACLE error number错误。 + +PRAGMA EXCEPTION_INIT不生成运行期语句。后续编译RAISE时保存异常datum指针,可以取得更新后的sqlcode;后续编译WHEN时将更新后的sqlcode写入条件节点。 + +=== 存储过程与包中的异常 + +独立存储过程编译时,异常datum存储在对应PLiSQL_function的datum数组中,并遵循过程声明区的命名空间范围。 + +包编译沿用现有的包命名空间和datum管理机制。编译包体时,package_body_init恢复包声明已有的命名空间、datum和子程序信息,因此包声明中的异常可以在包体中使用,包体级异常也可以被包内子程序引用。异常是常量标识符,不需要包状态初始化,也没有需要释放的运行期值。 + +copy_plisql_datums直接共享异常datum指针;plisql_free_function_memory和包资源清理代码只识别该datum类型,不对其执行普通变量值释放。plisql_dumptree通过plisql_dump_exception输出异常名称、dno、sqlcode和声明行号,用于调试编译结果。 + +=== 构建与回归测试 + +Makefile和meson.build均加入pl_exception_type.c。Oracle回归测试列表加入plisql_exception,测试输入和预期输出分别位于: + +``` +src/pl/plisql/src/sql/plisql_exception.sql +src/pl/plisql/src/expected/plisql_exception.out +``` + +回归测试实际执行异常抛出和捕获,并由命中的处理器向结果表写入记录。测试覆盖包级异常、过程局部异常、包内传播、多个异常、PRAGMA EXCEPTION_INIT、不同绑定错误码的处理器选择、错误码边界检查以及SQLERRM在包过程之间的传递。 + +=== 当前实现边界 + +当前实现的WHEN处理器按照sqlcode匹配,不按照异常datum的声明身份匹配。所有未使用PRAGMA EXCEPTION_INIT的用户异常共享P0001,因此两个未绑定的不同异常无法仅依靠错误码相互区分;绑定相同错误码的不同异常也具有相同的匹配结果。需要区分多个用户异常时,应通过PRAGMA EXCEPTION_INIT为它们设置不同的合法错误码。 + +PLiSQL_exception_var的sqlcode字段同时用于保存PostgreSQL内部SQLSTATE编码和PRAGMA指定的Oracle风格整数错误号。当前实现保证同一异常的RAISE与WHEN使用相同整数完成匹配,但没有提供独立的Oracle错误号到PostgreSQL SQLSTATE转换层。 diff --git a/CN/modules/ROOT/pages/master/oracle_compatibility/compat_user_defined_exception.adoc b/CN/modules/ROOT/pages/master/oracle_compatibility/compat_user_defined_exception.adoc new file mode 100644 index 00000000..3b14317e --- /dev/null +++ b/CN/modules/ROOT/pages/master/oracle_compatibility/compat_user_defined_exception.adoc @@ -0,0 +1,209 @@ +:sectnums: +:sectnumlevels: 5 + +:imagesdir: ./_images + += 用户自定义EXCEPTION + +== 目的 + +IvorySQL提供了兼容Oracle的用户自定义EXCEPTION功能,支持在PL/iSQL存储过程与包中声明用户自定义EXCEPTION。 + +本文档旨在为使用人员介绍此新增功能。 + +== 功能说明 + +IvorySQL提供的兼容Oracle的用户自定义EXCEPTION功能,包括如下内容。 + +=== 在包中声明自定义exception + +可以在包声明部分或包体声明部分使用 `异常名 EXCEPTION;` 声明用户自定义异常。声明在包声明部分的异常可以在对应包体中使用;只在包体中声明的异常用于包体内部的过程和函数。 + +语法如下: + +```sql +exception_name EXCEPTION; +``` + +自定义异常是PL/iSQL的异常对象,不是普通SQL数据类型,不能被赋值、作为表达式求值或作为过程返回值。异常名称遵循PL/iSQL的作用域规则,同一声明作用域内不能与其他变量或异常重名。 + +使用 `RAISE` 按名称抛出异常,并在 `EXCEPTION` 区域通过同一名称捕获: + +```sql +RAISE exception_name; + +EXCEPTION + WHEN exception_name THEN + handler_statement; +``` + +包级异常可以由包内的一个子程序抛出,再由包内调用它的另一个子程序捕获。异常向外传播时,当前语句块中的修改会按照PL/iSQL原有的异常子事务机制进行回滚,然后执行匹配的异常处理器。 + + +=== 在存储过程中声明exception + +可以在独立存储过程的声明区定义局部异常。局部异常只在声明它的过程及其嵌套作用域内可见,过程外不能直接引用该异常名称。 + +```sql +CREATE OR REPLACE PROCEDURE example_proc IS + local_exception EXCEPTION; +BEGIN + RAISE local_exception; +EXCEPTION + WHEN local_exception THEN + NULL; +END; +/ +``` + +执行 `RAISE local_exception` 时,IvorySQL创建错误并进入异常匹配流程。`WHEN local_exception` 命中后,可以在处理器中访问 `SQLERRM`。未通过 `PRAGMA EXCEPTION_INIT` 绑定错误码的用户异常使用内部SQLSTATE `P0001`;如果没有另外指定消息,`SQLERRM` 的默认内容是 `User-Defined Exception`。 + +异常处理器中使用不带参数的 `RAISE;`,可以继续向外层重新抛出当前异常。 + + +=== 把一个用户自定义异常名称与特定数据库错误码绑定 + +`PRAGMA EXCEPTION_INIT` 是编译期指令,用于把已经声明的用户自定义异常与指定错误码关联。它本身不是运行期语句,必须写在声明区,并且位于对应的 `EXCEPTION` 声明之后。 + +语法如下: + +```sql +exception_name EXCEPTION; +PRAGMA EXCEPTION_INIT(exception_name, error_code); +``` + +绑定后,执行 `RAISE exception_name` 时使用关联的错误码;`WHEN exception_name` 也根据该错误码进行匹配。这样可以用具有业务含义的名称代替数字错误码。 + +IvorySQL接受的错误码范围如下: + +* 正数只允许 `100`,用于ANSI `NO_DATA_FOUND`; +* 允许 `-1000000` 到 `-1` 之间的负整数,但不允许 `-1403`; +* 不允许 `0`、除 `100` 外的正整数以及小于 `-1000000` 的整数。 + +非法错误码会在编译存储过程或包时报告 `illegal ORACLE error number ... for PRAGMA EXCEPTION_INIT`。异常名称不存在,或者名称对应的对象不是异常时,也会在编译期报错。 + +错误码和错误消息相互独立。例如,`PRAGMA EXCEPTION_INIT(my_exception, -20001)` 只把 `-20001` 绑定到异常,并不会把 `SQLERRM` 自动转换成 `ORA-20001`。使用普通 `RAISE my_exception` 且未指定消息时,`SQLERRM` 仍为 `User-Defined Exception`。如需自定义消息,可以使用: + +```sql +RAISE my_exception USING MESSAGE = 'application error'; +``` + +=== 使用注意事项 + +IvorySQL当前通过错误码匹配 `WHEN` 处理器,而不是通过异常对象的声明身份匹配。所有未使用 `PRAGMA EXCEPTION_INIT` 的用户异常默认使用同一个内部SQLSTATE `P0001`。如果同一异常处理区域需要区分多个用户异常,应使用 `PRAGMA EXCEPTION_INIT` 为它们绑定不同的合法错误码;绑定相同错误码的异常也无法在运行期相互区分。 + + +== 测试用例 + +=== 在包中声明自定义exception + +下面的用例实际抛出并捕获包级异常。只有匹配的异常处理器得到执行,才会向结果表写入记录。 + +``` +CREATE TABLE plisql_exception_results +( + test_no NUMBER, + test_name VARCHAR2(40), + caught_by VARCHAR2(40), + detail VARCHAR2(100) +); + +CREATE OR REPLACE PACKAGE test_exc_pkg1 IS + PROCEDURE test_proc; +END test_exc_pkg1; +/ + +CREATE OR REPLACE PACKAGE BODY test_exc_pkg1 IS + bad_interval EXCEPTION; + + PROCEDURE test_proc IS + BEGIN + RAISE bad_interval; + EXCEPTION + WHEN bad_interval THEN + INSERT INTO plisql_exception_results + VALUES (1, 'package_basic', 'bad_interval', SQLERRM); + END test_proc; +END test_exc_pkg1; +/ + +BEGIN + test_exc_pkg1.test_proc(); +END; +/ +``` +=== 在存储过程中声明exception + +``` +CREATE OR REPLACE PROCEDURE test_standalone_exc IS + my_exception EXCEPTION; +BEGIN + RAISE my_exception; +EXCEPTION + WHEN my_exception THEN + INSERT INTO plisql_exception_results + VALUES (3, 'standalone', 'my_exception', SQLERRM); +END; +/ + +BEGIN + test_standalone_exc(); +END; +/ +``` + +=== 把一个用户自定义异常名称与特定数据库错误码绑定 + +``` +CREATE OR REPLACE PACKAGE test_pragma_init IS + PROCEDURE test_basic_pragma; +END test_pragma_init; +/ + +CREATE OR REPLACE PACKAGE BODY test_pragma_init IS + my_exception EXCEPTION; + PRAGMA EXCEPTION_INIT(my_exception, -20001); + + PROCEDURE test_basic_pragma IS + BEGIN + RAISE my_exception; + EXCEPTION + WHEN my_exception THEN + INSERT INTO plisql_exception_results + VALUES (6, 'pragma_basic', 'my_exception', SQLERRM); + END test_basic_pragma; +END test_pragma_init; +/ + +BEGIN + test_pragma_init.test_basic_pragma(); +END; +/ +``` + +执行以上三个用例后,可以查询实际进入的异常处理器及 `SQLERRM`: + +```sql +SELECT test_name, caught_by, detail +FROM plisql_exception_results +ORDER BY test_no; +``` + +预期结果为: + +``` + test_name | caught_by | detail +-----------------+---------------+------------------------ + package_basic | bad_interval | User-Defined Exception + standalone | my_exception | User-Defined Exception + pragma_basic | my_exception | User-Defined Exception +``` + +测试结束后清理对象: + +```sql +DROP PACKAGE test_exc_pkg1; +DROP PROCEDURE test_standalone_exc; +DROP PACKAGE test_pragma_init; +DROP TABLE plisql_exception_results; +``` diff --git a/EN/modules/ROOT/nav.adoc b/EN/modules/ROOT/nav.adoc index 594bda81..d96388ba 100644 --- a/EN/modules/ROOT/nav.adoc +++ b/EN/modules/ROOT/nav.adoc @@ -31,7 +31,8 @@ ** xref:master/oracle_compatibility/compat_create_index_online.adoc[22、ONLINE Parameter for CREATE INDEX] ** xref:master/oracle_compatibility/compat_stragg.adoc[23、STRAGG function] ** xref:master/oracle_compatibility/compat_alter_index_unusable_en.adoc[24、Alter Index Unusable] -** xref:master/oracle_compatibility/compat_dbtimezone_en.adoc[24、dbtimezone] +** xref:master/oracle_compatibility/compat_dbtimezone_en.adoc[25、dbtimezone] +** xref:master/oracle_compatibility/compat_user_defined_exception_en.adoc[26、User Defined EXCEPTION] * Containerization and Cloud Service ** Containerization *** xref:master/containerization/k8s_deployment.adoc[K8S deployment] @@ -112,6 +113,7 @@ *** xref:master/compatibility_features_design/with_function_procedure_impl_en.adoc[WITH FUNCTION/PROCEDURE] *** xref:master/compatibility_features_design/create_index_online.adoc[ONLINE Parameter for CREATE INDEX] *** xref:master/compatibility_features_design/alter_index_unusable_impl_en.adoc[Alter Index Unusable] +*** xref:master/compatibility_features_design/user_defined_exception_en.adoc[User Defined EXCEPTION] ** Built-in Functions *** xref:master/oracle_builtin_functions/sys_context.adoc[sys_context] *** xref:master/oracle_builtin_functions/userenv.adoc[userenv] diff --git a/EN/modules/ROOT/pages/master/compatibility_features_design/alter_index_unusable_impl_en.adoc b/EN/modules/ROOT/pages/master/compatibility_features_design/alter_index_unusable_impl.adoc similarity index 100% rename from EN/modules/ROOT/pages/master/compatibility_features_design/alter_index_unusable_impl_en.adoc rename to EN/modules/ROOT/pages/master/compatibility_features_design/alter_index_unusable_impl.adoc diff --git a/EN/modules/ROOT/pages/master/compatibility_features_design/user_defined_exception.adoc b/EN/modules/ROOT/pages/master/compatibility_features_design/user_defined_exception.adoc new file mode 100644 index 00000000..dacdfc2e --- /dev/null +++ b/EN/modules/ROOT/pages/master/compatibility_features_design/user_defined_exception.adoc @@ -0,0 +1,140 @@ +:sectnums: +:sectnumlevels: 5 + + += **Feature Overview** + +IvorySQL provides an Oracle-compatible user-defined EXCEPTION feature. It supports declaring exceptions in PL/iSQL stored procedures, package specifications, and package bodies; raising an exception by name with RAISE; catching it by name with WHEN; and associating an exception name with a specified error code through PRAGMA EXCEPTION_INIT. + +Basic usage: + +```sql +CREATE OR REPLACE PROCEDURE test_exception IS + my_exception EXCEPTION; + PRAGMA EXCEPTION_INIT(my_exception, -20001); +BEGIN + RAISE my_exception; +EXCEPTION + WHEN my_exception THEN + RAISE INFO 'caught: %', SQLERRM; +END; +/ +``` + +== Implementation + +=== User-Defined EXCEPTION Data Structure + +`PLISQL_DTYPE_EXCEPTION` is added to the `PLiSQL_datum_type` enumeration to distinguish user-defined exceptions from ordinary variables, records, and package variables. + +The `PLiSQL_exception_var` structure is defined in `pl_exception_type.h`: + +``` +typedef struct PLiSQL_exception_var +{ + PLiSQL_datum_type dtype; + int dno; + Oid pkgoid; + char *refname; + int lineno; + int sqlcode; +} PLiSQL_exception_var; +``` + +`dno` is the exception's index in the datum array of the current PL/iSQL compilation unit. `refname` stores the exception name, `lineno` stores the declaration location, and `sqlcode` stores the error code used when raising and catching the exception. + +`plisql_build_exception` creates the exception datum, adds it to the compiler datum array through `plisql_adddatum`, and adds it to the current namespace as `PLISQL_NSTYPE_VAR`. The namespace type used for ordinary variables is reused; the datum's `PLISQL_DTYPE_EXCEPTION` type determines whether the object is an exception. + +Without `PRAGMA EXCEPTION_INIT`, the exception's `sqlcode` is initialized to `ERRCODE_RAISE_EXCEPTION`, which corresponds to the internal PostgreSQL SQLSTATE `P0001`. An exception datum stores only compile-time metadata and has no mutable runtime value. + +=== User-Defined EXCEPTION Declaration Syntax + +The following production is added to the declaration statements in `pl_gram.y`: + +``` +decl_varname K_EXCEPTION ';' +``` + +When this syntax is parsed, `plisql_build_exception` creates the exception and registers it in the current PL/iSQL namespace. Exception names therefore use the existing lexical scope and duplicate-name rules: an inner scope can look up an exception in an outer scope, while objects with the same name cannot be declared more than once in the same scope. + +An exception is a special PL/iSQL identifier rather than a PostgreSQL data type. `PLISQL_DTYPE_EXCEPTION` branches are added to the executor paths for variable initialization, datum copying, function-memory cleanup, and package-resource cleanup. Exceptions do not require initialization at block entry, and no runtime value is copied. Paths in which an exception cannot be used, such as assignment and OUT-parameter processing, either reject the type or skip irrelevant processing. + +=== Raising a User-Defined Exception + +An `exception_var` member is added to `PLiSQL_stmt_raise` to store the `PLiSQL_exception_var` pointer resolved at compile time. + +When a RAISE statement is compiled, if the lexer returns `T_DATUM` and the datum has type `PLISQL_DTYPE_EXCEPTION`, the exception is stored in `exception_var` instead of being processed as a predefined exception-condition name: + +``` +RAISE exception_name; +``` + +At execution time, `exec_stmt_raise` obtains the `sqlcode` and exception name from `exception_var` and writes the `sqlcode` to the error-code field used by `ErrorData`. If no explicit message is supplied, the default message for a user-defined exception is `User-Defined Exception`; consequently, `SQLERRM` in the exception handler returns the same string. + +The error code and error message are processed separately. `PRAGMA EXCEPTION_INIT` changes only the error code and does not automatically produce an ORA message from that code. The RAISE `MESSAGE` option can override the default message: + +```sql +RAISE my_exception USING MESSAGE = 'application error'; +``` + +An unqualified RAISE with no other parameters continues to rethrow the current exception from an exception handler. To prevent `RAISE exception_name` from being mistaken for an unqualified RAISE, the rethrow condition also checks whether `exception_var` is null. + +=== Catching a User-Defined Exception with WHEN + +When parsing a WHEN condition, `plisql_parse_err_condition` first calls `plisql_lookup_exception` to search the current namespace for a user-defined exception. It checks `OTHERS` and predefined exception conditions only when no user-defined exception is found. + +When a user-defined exception is found, the compiler creates a `PLiSQL_condition` and copies the `sqlcode` from `PLiSQL_exception_var` into `PLiSQL_condition.sqlerrstate`: + +``` +EXCEPTION + WHEN exception_name THEN + handler_statement; +``` + +Runtime processing reuses the existing PL/iSQL exception-block implementation. A block containing an EXCEPTION section runs in an internal subtransaction. When an error occurs, the internal subtransaction is rolled back and `ErrorData` is copied. `exception_matches_conditions` then compares `ErrorData.sqlerrcode` with `PLiSQL_condition.sqlerrstate`. After a match, the executor initializes `SQLSTATE`, `SQLERRM`, and the current error information before running the corresponding handler statements. + +By reusing the existing processing path, user-defined exceptions automatically support exception-block rollback, `SQLERRM`, `SQLSTATE`, and rethrowing through an unqualified RAISE in a handler. + +=== Associating an Error Code with PRAGMA EXCEPTION_INIT + +Support for the `PRAGMA` and `EXCEPTION_INIT` keywords is added to the keyword and grammar files. The grammar uses `any_identifier` to reference an already declared exception and accepts both positive and negative integers: + +``` +PRAGMA EXCEPTION_INIT(exception_name, error_code); +``` + +`plisql_process_pragma_exception_init` performs the following work at compile time: + +1. Searches the current namespace for `exception_name`. +2. Verifies that the corresponding datum has type `PLISQL_DTYPE_EXCEPTION`. +3. Calls `plisql_validate_exception_error_code` to validate the error code. +4. Calls `plisql_exception_set_sqlcode` to update the `sqlcode` in the exception datum. + +The error-code validation rules are as follows: `100` is the only accepted positive value; negative values from `-1000000` through `-1` are accepted except for `-1403`; zero, positive values other than `100`, and values below `-1000000` are rejected. An invalid error code produces an `illegal ORACLE error number` error at compile time. + +`PRAGMA EXCEPTION_INIT` does not generate a runtime statement. A subsequently compiled RAISE statement stores the exception datum pointer and can therefore read the updated `sqlcode`. A subsequently compiled WHEN condition writes the updated `sqlcode` into its condition node. + +=== Exceptions in Stored Procedures and Packages + +When a standalone stored procedure is compiled, its exception datums are stored in the datum array of the corresponding `PLiSQL_function` and follow the namespace scope of the procedure's declaration section. + +Package compilation reuses the existing package namespace and datum-management mechanism. When a package body is compiled, `package_body_init` restores the namespace, datums, and subprogram information from the package specification. An exception declared in a package specification can therefore be used in the package body, and package-body exceptions can be referenced by subprograms in that body. Exceptions are constant identifiers, require no package-state initialization, and contain no runtime value that must be released. + +`copy_plisql_datums` shares the exception datum pointer directly. `plisql_free_function_memory` and the package-resource cleanup code recognize the datum type but do not perform ordinary variable-value cleanup on it. For debugging compiled output, `plisql_dumptree` calls `plisql_dump_exception` to display the exception name, `dno`, `sqlcode`, and declaration line. + +=== Build and Regression Tests + +Both `Makefile` and `meson.build` include `pl_exception_type.c`. The `plisql_exception` test is registered in the Oracle regression-test list. Its input and expected-output files are: + +``` +src/pl/plisql/src/sql/plisql_exception.sql +src/pl/plisql/src/expected/plisql_exception.out +``` + +The regression test actually raises and catches exceptions, and each matching handler writes a row to a results table. It covers package-level exceptions, procedure-local exceptions, propagation within a package, multiple exceptions, `PRAGMA EXCEPTION_INIT`, handler selection for different associated error codes, error-code boundary validation, and passing `SQLERRM` between package procedures. + +=== Current Implementation Boundaries + +The current implementation matches WHEN handlers by `sqlcode`, not by the declaration identity of an exception datum. All user-defined exceptions without `PRAGMA EXCEPTION_INIT` share `P0001`; consequently, two different unassociated exceptions cannot be distinguished using only their error codes. Different exceptions associated with the same error code also produce the same matching result. Associate exceptions with distinct valid error codes through `PRAGMA EXCEPTION_INIT` when they must be distinguished. + +The `sqlcode` field in `PLiSQL_exception_var` is used both for internal PostgreSQL SQLSTATE encodings and for Oracle-style integer error codes specified by PRAGMA. The current implementation ensures that RAISE and WHEN for the same exception use the same integer for matching, but it does not provide a separate conversion layer from Oracle error numbers to PostgreSQL SQLSTATE values. diff --git a/EN/modules/ROOT/pages/master/compatibility_features_design/with_function_procedure_impl_en.adoc b/EN/modules/ROOT/pages/master/compatibility_features_design/with_function_procedure_impl.adoc similarity index 100% rename from EN/modules/ROOT/pages/master/compatibility_features_design/with_function_procedure_impl_en.adoc rename to EN/modules/ROOT/pages/master/compatibility_features_design/with_function_procedure_impl.adoc diff --git a/EN/modules/ROOT/pages/master/ecosystem_components/pg_readonly_en.adoc b/EN/modules/ROOT/pages/master/ecosystem_components/pg_readonly.adoc similarity index 100% rename from EN/modules/ROOT/pages/master/ecosystem_components/pg_readonly_en.adoc rename to EN/modules/ROOT/pages/master/ecosystem_components/pg_readonly.adoc diff --git a/EN/modules/ROOT/pages/master/ecosystem_components/zhparser_en.adoc b/EN/modules/ROOT/pages/master/ecosystem_components/zhparser.adoc similarity index 100% rename from EN/modules/ROOT/pages/master/ecosystem_components/zhparser_en.adoc rename to EN/modules/ROOT/pages/master/ecosystem_components/zhparser.adoc diff --git a/EN/modules/ROOT/pages/master/oracle_builtin_functions/dbtimezone_impl_en.adoc b/EN/modules/ROOT/pages/master/oracle_builtin_functions/dbtimezone_impl.adoc similarity index 100% rename from EN/modules/ROOT/pages/master/oracle_builtin_functions/dbtimezone_impl_en.adoc rename to EN/modules/ROOT/pages/master/oracle_builtin_functions/dbtimezone_impl.adoc diff --git a/EN/modules/ROOT/pages/master/oracle_builtin_functions/vsize_en.adoc b/EN/modules/ROOT/pages/master/oracle_builtin_functions/vsize.adoc similarity index 100% rename from EN/modules/ROOT/pages/master/oracle_builtin_functions/vsize_en.adoc rename to EN/modules/ROOT/pages/master/oracle_builtin_functions/vsize.adoc diff --git a/EN/modules/ROOT/pages/master/oracle_compatibility/compat_alter_index_unusable_en.adoc b/EN/modules/ROOT/pages/master/oracle_compatibility/compat_alter_index_unusable.adoc similarity index 100% rename from EN/modules/ROOT/pages/master/oracle_compatibility/compat_alter_index_unusable_en.adoc rename to EN/modules/ROOT/pages/master/oracle_compatibility/compat_alter_index_unusable.adoc diff --git a/EN/modules/ROOT/pages/master/oracle_compatibility/compat_dbtimezone_en.adoc b/EN/modules/ROOT/pages/master/oracle_compatibility/compat_dbtimezone.adoc similarity index 100% rename from EN/modules/ROOT/pages/master/oracle_compatibility/compat_dbtimezone_en.adoc rename to EN/modules/ROOT/pages/master/oracle_compatibility/compat_dbtimezone.adoc diff --git a/EN/modules/ROOT/pages/master/oracle_compatibility/compat_user_defined_exception.adoc b/EN/modules/ROOT/pages/master/oracle_compatibility/compat_user_defined_exception.adoc new file mode 100644 index 00000000..9202f517 --- /dev/null +++ b/EN/modules/ROOT/pages/master/oracle_compatibility/compat_user_defined_exception.adoc @@ -0,0 +1,210 @@ +:sectnums: +:sectnumlevels: 5 + +:imagesdir: ./_images + += User-Defined EXCEPTION + +== Purpose + +IvorySQL provides an Oracle-compatible user-defined EXCEPTION feature that supports declaring custom exceptions in PL/iSQL stored procedures and packages. + +This document introduces the feature to users. + +== Feature Description + +The Oracle-compatible user-defined EXCEPTION feature provided by IvorySQL includes the following capabilities. + +=== Declaring a Custom Exception in a Package + +A user-defined exception can be declared in a package specification or package body with `exception_name EXCEPTION;`. An exception declared in the package specification can be used in the corresponding package body. An exception declared only in the package body is available to procedures and functions within that package body. + +Syntax: + +```sql +exception_name EXCEPTION; +``` + +A user-defined exception is a PL/iSQL exception object, not an ordinary SQL data type. It cannot be assigned a value, evaluated as an expression, or used as a procedure return value. Exception names follow PL/iSQL scope rules and cannot have the same name as another variable or exception in the same declaration scope. + +Use `RAISE` to raise an exception by name and use the same name in the `EXCEPTION` section to catch it: + +```sql +RAISE exception_name; + +EXCEPTION + WHEN exception_name THEN + handler_statement; +``` + +A package-level exception can be raised by one subprogram in a package and caught by another package subprogram that calls it. When an exception propagates, changes made in the current block are rolled back through the existing PL/iSQL exception subtransaction mechanism before the matching exception handler is executed. + + +=== Declaring an Exception in a Stored Procedure + +A local exception can be defined in the declaration section of a standalone stored procedure. It is visible only within the procedure that declares it and its nested scopes; its name cannot be referenced directly outside the procedure. + +```sql +CREATE OR REPLACE PROCEDURE example_proc IS + local_exception EXCEPTION; +BEGIN + RAISE local_exception; +EXCEPTION + WHEN local_exception THEN + NULL; +END; +/ +``` + +When `RAISE local_exception` is executed, IvorySQL creates an error and enters the exception-matching process. After `WHEN local_exception` matches, the handler can access `SQLERRM`. A user-defined exception that is not associated with an error code by `PRAGMA EXCEPTION_INIT` uses the internal SQLSTATE `P0001`. If no other message is specified, the default value of `SQLERRM` is `User-Defined Exception`. + +An unqualified `RAISE;` in an exception handler rethrows the current exception to an outer scope. + + +=== Associating a User-Defined Exception Name with an Error Code + +`PRAGMA EXCEPTION_INIT` is a compile-time directive that associates an already declared user-defined exception with a specified error code. It is not a runtime statement. It must appear in the declaration section after the corresponding `EXCEPTION` declaration. + +Syntax: + +```sql +exception_name EXCEPTION; +PRAGMA EXCEPTION_INIT(exception_name, error_code); +``` + +After the association is established, `RAISE exception_name` uses the associated error code, and `WHEN exception_name` matches that error code. This allows a meaningful name to be used instead of a numeric error code. + +IvorySQL accepts error codes according to the following rules: + +* `100` is the only accepted positive value and represents ANSI `NO_DATA_FOUND`. +* Negative integers from `-1000000` through `-1` are accepted, except for `-1403`. +* Zero, positive integers other than `100`, and integers less than `-1000000` are rejected. + +An invalid error code causes `illegal ORACLE error number ... for PRAGMA EXCEPTION_INIT` to be reported while the stored procedure or package is compiled. A compile-time error is also reported if the exception name does not exist or names an object that is not an exception. + +The error code and error message are independent. For example, `PRAGMA EXCEPTION_INIT(my_exception, -20001)` only associates `-20001` with the exception; it does not automatically convert `SQLERRM` into `ORA-20001`. When a plain `RAISE my_exception` is used without an explicit message, `SQLERRM` remains `User-Defined Exception`. To provide a custom message, use: + +```sql +RAISE my_exception USING MESSAGE = 'application error'; +``` + +=== Usage Notes + +IvorySQL currently matches a `WHEN` handler by error code rather than by the declaration identity of the exception object. All user-defined exceptions without `PRAGMA EXCEPTION_INIT` use the same internal SQLSTATE, `P0001`. If multiple user-defined exceptions must be distinguished in the same exception-handling section, associate each one with a different valid error code through `PRAGMA EXCEPTION_INIT`. Exceptions associated with the same error code also cannot be distinguished at runtime. + + +== Test Cases + +=== Declaring a Custom Exception in a Package + +The following test actually raises and catches a package-level exception. A row is inserted into the results table only if the matching exception handler is executed. + +``` +CREATE TABLE plisql_exception_results +( + test_no NUMBER, + test_name VARCHAR2(40), + caught_by VARCHAR2(40), + detail VARCHAR2(100) +); + +CREATE OR REPLACE PACKAGE test_exc_pkg1 IS + PROCEDURE test_proc; +END test_exc_pkg1; +/ + +CREATE OR REPLACE PACKAGE BODY test_exc_pkg1 IS + bad_interval EXCEPTION; + + PROCEDURE test_proc IS + BEGIN + RAISE bad_interval; + EXCEPTION + WHEN bad_interval THEN + INSERT INTO plisql_exception_results + VALUES (1, 'package_basic', 'bad_interval', SQLERRM); + END test_proc; +END test_exc_pkg1; +/ + +BEGIN + test_exc_pkg1.test_proc(); +END; +/ +``` + +=== Declaring an Exception in a Stored Procedure + +``` +CREATE OR REPLACE PROCEDURE test_standalone_exc IS + my_exception EXCEPTION; +BEGIN + RAISE my_exception; +EXCEPTION + WHEN my_exception THEN + INSERT INTO plisql_exception_results + VALUES (3, 'standalone', 'my_exception', SQLERRM); +END; +/ + +BEGIN + test_standalone_exc(); +END; +/ +``` + +=== Associating a User-Defined Exception Name with an Error Code + +``` +CREATE OR REPLACE PACKAGE test_pragma_init IS + PROCEDURE test_basic_pragma; +END test_pragma_init; +/ + +CREATE OR REPLACE PACKAGE BODY test_pragma_init IS + my_exception EXCEPTION; + PRAGMA EXCEPTION_INIT(my_exception, -20001); + + PROCEDURE test_basic_pragma IS + BEGIN + RAISE my_exception; + EXCEPTION + WHEN my_exception THEN + INSERT INTO plisql_exception_results + VALUES (6, 'pragma_basic', 'my_exception', SQLERRM); + END test_basic_pragma; +END test_pragma_init; +/ + +BEGIN + test_pragma_init.test_basic_pragma(); +END; +/ +``` + +After running the three tests above, query the handlers that were actually entered and their `SQLERRM` values: + +```sql +SELECT test_name, caught_by, detail +FROM plisql_exception_results +ORDER BY test_no; +``` + +Expected result: + +``` + test_name | caught_by | detail +-----------------+---------------+------------------------ + package_basic | bad_interval | User-Defined Exception + standalone | my_exception | User-Defined Exception + pragma_basic | my_exception | User-Defined Exception +``` + +Clean up the objects after the tests: + +```sql +DROP PACKAGE test_exc_pkg1; +DROP PROCEDURE test_standalone_exc; +DROP PACKAGE test_pragma_init; +DROP TABLE plisql_exception_results; +``` diff --git a/EN/modules/ROOT/pages/master/oracle_compatibility/with_function_procedure_en.adoc b/EN/modules/ROOT/pages/master/oracle_compatibility/with_function_procedure.adoc similarity index 100% rename from EN/modules/ROOT/pages/master/oracle_compatibility/with_function_procedure_en.adoc rename to EN/modules/ROOT/pages/master/oracle_compatibility/with_function_procedure.adoc From 8d2b37410708ed53d0b825d970ae4147108a9965 Mon Sep 17 00:00:00 2001 From: Steven Niu Date: Mon, 21 Sep 2026 09:32:02 +0000 Subject: [PATCH 2/2] use correct file name in nav.adoc of english version --- EN/modules/ROOT/nav.adoc | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/EN/modules/ROOT/nav.adoc b/EN/modules/ROOT/nav.adoc index d96388ba..bbfa437d 100644 --- a/EN/modules/ROOT/nav.adoc +++ b/EN/modules/ROOT/nav.adoc @@ -27,12 +27,12 @@ ** xref:master/oracle_compatibility/compat_empty_string_to_null.adoc[18、Empty String to NULL] ** xref:master/oracle_compatibility/compat_call_into.adoc[19、CALL INTO] ** xref:master/oracle_compatibility/compat_read_only_view.adoc[20、Read Only View] -** xref:master/oracle_compatibility/with_function_procedure_en.adoc[21、WITH FUNCTION/PROCEDURE] +** xref:master/oracle_compatibility/with_function_procedure.adoc[21、WITH FUNCTION/PROCEDURE] ** xref:master/oracle_compatibility/compat_create_index_online.adoc[22、ONLINE Parameter for CREATE INDEX] ** xref:master/oracle_compatibility/compat_stragg.adoc[23、STRAGG function] -** xref:master/oracle_compatibility/compat_alter_index_unusable_en.adoc[24、Alter Index Unusable] -** xref:master/oracle_compatibility/compat_dbtimezone_en.adoc[25、dbtimezone] -** xref:master/oracle_compatibility/compat_user_defined_exception_en.adoc[26、User Defined EXCEPTION] +** xref:master/oracle_compatibility/compat_alter_index_unusable.adoc[24、Alter Index Unusable] +** xref:master/oracle_compatibility/compat_dbtimezone.adoc[25、dbtimezone] +** xref:master/oracle_compatibility/compat_user_defined_exception.adoc[26、User Defined EXCEPTION] * Containerization and Cloud Service ** Containerization *** xref:master/containerization/k8s_deployment.adoc[K8S deployment] @@ -73,8 +73,8 @@ *** xref:master/ecosystem_components/pg_profile.adoc[pg_profile] *** xref:master/ecosystem_components/pg_repack.adoc[pg_repack] *** xref:master/ecosystem_components/pgdog.adoc[PgDog] -*** xref:master/ecosystem_components/pg_readonly_en.adoc[pg_readonly] -*** xref:master/ecosystem_components/zhparser_en.adoc[zhparser] +*** xref:master/ecosystem_components/pg_readonly.adoc[pg_readonly] +*** xref:master/ecosystem_components/zhparser.adoc[zhparser] *** xref:master/ecosystem_components/pgbackrest.adoc[pgBackRest] *** xref:master/ecosystem_components/set_user.adoc[set_user] *** xref:master/ecosystem_components/pg_jieba.adoc[pg_jieba] @@ -110,17 +110,17 @@ *** xref:master/compatibility_features_design/empty_string_to_null.adoc[Empty String to NULL] *** xref:master/compatibility_features_design/call_into.adoc[CALL INTO] *** xref:master/compatibility_features_design/read_only_view.adoc[Read Only View] -*** xref:master/compatibility_features_design/with_function_procedure_impl_en.adoc[WITH FUNCTION/PROCEDURE] +*** xref:master/compatibility_features_design/with_function_procedure_impl.adoc[WITH FUNCTION/PROCEDURE] *** xref:master/compatibility_features_design/create_index_online.adoc[ONLINE Parameter for CREATE INDEX] -*** xref:master/compatibility_features_design/alter_index_unusable_impl_en.adoc[Alter Index Unusable] -*** xref:master/compatibility_features_design/user_defined_exception_en.adoc[User Defined EXCEPTION] +*** xref:master/compatibility_features_design/alter_index_unusable_impl.adoc[Alter Index Unusable] +*** xref:master/compatibility_features_design/user_defined_exception.adoc[User Defined EXCEPTION] ** Built-in Functions *** xref:master/oracle_builtin_functions/sys_context.adoc[sys_context] *** xref:master/oracle_builtin_functions/userenv.adoc[userenv] *** xref:master/oracle_builtin_functions/rawtohex.adoc[rawtohex] *** xref:master/oracle_builtin_functions/stragg.adoc[stragg] -*** xref:master/oracle_builtin_functions/dbtimezone_impl_en.adoc[dbtimezone] -*** xref:master/oracle_builtin_functions/vsize_en.adoc[vsize] +*** xref:master/oracle_builtin_functions/dbtimezone_impl.adoc[dbtimezone] +*** xref:master/oracle_builtin_functions/vsize.adoc[vsize] ** xref:master/gb18030.adoc[GB18030 Character Set] * Reference ** xref:master/tools_reference.adoc[Tool Reference]