From 118fae2507727dd6e02abdca26d6bb32e3f87e07 Mon Sep 17 00:00:00 2001 From: himmel Date: Wed, 23 Sep 2026 16:52:59 +0800 Subject: [PATCH] docs: package_record_type update --- CN/modules/ROOT/nav.adoc | 2 + .../package_record_type.adoc | 130 ++++++++ .../package_record_type.adoc | 287 ++++++++++++++++++ EN/modules/ROOT/nav.adoc | 2 + .../package_record_type.adoc | 130 ++++++++ .../package_record_type.adoc | 287 ++++++++++++++++++ 6 files changed, 838 insertions(+) create mode 100644 CN/modules/ROOT/pages/master/compatibility_features_design/package_record_type.adoc create mode 100644 CN/modules/ROOT/pages/master/oracle_compatibility/package_record_type.adoc create mode 100644 EN/modules/ROOT/pages/master/compatibility_features_design/package_record_type.adoc create mode 100644 EN/modules/ROOT/pages/master/oracle_compatibility/package_record_type.adoc diff --git a/CN/modules/ROOT/nav.adoc b/CN/modules/ROOT/nav.adoc index 7ed31aff..59f191a6 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/package_record_type.adoc[27、包中的 RECORD 类型声明] * 容器化与云服务 ** 容器化指南 *** 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/package_record_type.adoc[包 RECORD 类型] *** 内置函数 **** 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/package_record_type.adoc b/CN/modules/ROOT/pages/master/compatibility_features_design/package_record_type.adoc new file mode 100644 index 00000000..24b91ee5 --- /dev/null +++ b/CN/modules/ROOT/pages/master/compatibility_features_design/package_record_type.adoc @@ -0,0 +1,130 @@ +:sectnums: +:sectnumlevels: 5 + += 包 RECORD 类型设计 + +== 背景 + +=== Oracle 语义 + +Oracle PL/SQL 允许通过 `TYPE ... IS RECORD` 定义由多个字段组成的记录类型。记录类型可在包规范、包体和 PL/SQL 块中声明,并可作为变量、参数和函数返回值的数据类型。包规范中的类型还可以通过包限定名称供其他程序单元使用。 + +=== 目的 + +在迁移 Oracle 应用时,公共类型包和以记录为参数或返回值的包接口较为常见。支持包记录类型后,可保留这类接口及其字段访问方式,减少将记录拆分为多个标量参数或额外创建数据库复合类型的改造工作。 + +== 架构设计 + +=== 设计选择 + +解析 `TYPE ... IS RECORD` 时,PL/iSQL 根据字段列表构造 `TupleDesc`,完成描述符初始化后调用 `BlessTupleDesc()`,为匿名 `RECORD` 分配可解析的 typmod。由此得到的记录类型可以在编译阶段保留完整字段结构,并用于变量声明、子程序参数和返回类型。 + +类型声明与记录变量使用不同的命名空间项: + +[cols="2,2,4",options="header"] +|=== +|对象 |命名空间类型 |作用 +|记录类型声明 |`PLISQL_NSTYPE_ROWTYPE` |保存类型字段列表和对应的元组描述符。 +|记录变量 |`PLISQL_NSTYPE_REC` |保存运行期记录值及字段访问状态。 +|=== + +解析器据此区分类型名和变量名。`type_name.field`、`type_name%TYPE` 等无效用法会返回编译错误,不会访问不兼容的数据结构。 + +=== 可见性和类型解析 + +* 在包规范中声明的记录类型可作为公共包成员,通过 `package_name.type_name` 解析。 +* 在包体中声明的记录类型仅加入包体的内部命名空间。 +* 同一个包内部既可以使用非限定类型名,也可以使用自身包名限定类型名。 +* 跨包引用在目标包的已编译类型信息中查找记录类型,并保留其 typmod 和内存上下文生命周期。 +* 类型名参与普通的重复声明检查,不能与同一作用域内的类型或变量重名。 + +=== 代码组织 + +[source,text] +---- +src/pl/plisql/src/ +├── pl_gram.y -- TYPE ... IS RECORD 语法和字段检查 +├── pl_comp.c -- 类型查找、列引用和嵌套字段解析 +├── pl_package.c -- 包类型注册及包限定类型解析 +├── pl_subproc_function.c -- 子程序参数和返回类型处理 +├── pl_exec.c -- 记录实例化和元组描述符生命周期 +├── plisql.h -- 命名空间类型定义 +├── pl_unreserved_kwlist.h -- 相关非保留关键字 +├── sql/ +│ └── plisql_package_type_record.sql -- 回归测试 SQL +└── expected/ + └── plisql_package_type_record.out -- 期望输出 +---- + +== 实现细节 + +=== 声明阶段 + +语法分析器读取记录字段的名称和数据类型,为声明创建 `PLiSQL_row`。字段解析使用独立的临时命名空间,因此字段名不会泄漏到外围包或块作用域,也不会与外围局部变量产生伪冲突。 + +字段列表完成后,系统构造并完成 `TupleDesc`,再将其注册为带 typmod 的匿名 `RECORDOID`。记录变量即使尚未执行整行赋值,也能根据该描述符实例化并逐字段写入。 + +=== 参数和返回值 + +包记录类型的 OID 为 `RECORDOID`,具体结构由正 typmod 标识。类型解析路径同时传递 OID 和 typmod,使以下位置都能获得相同的字段结构: + +* 局部变量和包变量; +* `IN`、`OUT` 参数; +* 函数返回类型; +* 同包限定引用和跨包引用。 + +函数返回包记录类型时,查询规划器可以从 typmod 恢复复合结构。因此,`SELECT * FROM package_name.function_name(...)` 不需要额外的列定义列表。 + +=== 嵌套复合字段 + +记录字段的类型解析结果包含真实的类型 OID 和 typmod,而不是统一退化为无结构的 `RECORDOID/-1`。`resolve_column_ref()` 在处理多级字段引用时构造 `FieldSelect`,从而支持 `record.field.subfield` 形式的嵌套读取。 + +如果内层字段是通过 `CREATE TYPE ... AS` 定义的命名复合类型,其 `pg_type.typrelid` 可以在执行阶段恢复结构,因此嵌套读取和逐字段写入均可使用。 + +如果内层字段是另一个包记录类型,则整字段赋值和嵌套读取可用,但逐字段写入受到 PostgreSQL 核心 `FieldStore` 节点的限制。`FieldStore` 不保存结果 typmod,执行阶段以 typmod `-1` 查找类型,无法恢复仅通过正 typmod 注册的匿名包记录结构。当前实现对此返回明确错误。 + +=== 元组描述符生命周期 + +包记录类型和命名复合类型的 `TupleDesc` 所有权不同: + +* 包记录类型通过行类型查找取得引用计数描述符,使用后需要释放。 +* 命名复合类型可能返回由类型缓存持有的描述符,调用方不得释放。 + +实现根据描述符来源分别处理所有权,避免错误释放类型缓存中的描述符。跨包和自身包限定类型引用所需的数据会复制到生命周期足够长的内存上下文中,保证包缓存有效期间类型信息不会失效。 + +== 错误处理 + +=== 不支持的字段子句 + +记录字段暂不支持 `NOT NULL` 和 `DEFAULT`: + +* `PLiSQL_row` 没有保存逐字段默认表达式的结构; +* 在没有字段默认值的情况下,实例化记录时无法满足字段级 `NOT NULL` 约束。 + +语法分析器会在编译阶段拒绝这两种子句,避免静默忽略默认值或在记录实例化时触发断言错误。 + +=== TABLE OF 声明 + +语法分析器能够识别 `TYPE name IS TABLE OF ...`,但集合类型尚未实现。系统返回带类型名的“不支持”错误,而不是通用语法错误。 + +=== 类型与变量混淆 + +由于类型声明和记录变量具有独立的命名空间类型,以下错误会被显式拒绝: + +* 将类型名作为记录值并访问字段; +* 对类型声明本身使用 `%TYPE`; +* 在包限定类型名后追加不存在的字段或名称; +* 在同一作用域重复声明类型名,或让类型名与变量名冲突。 + +== 测试覆盖 + +`plisql_package_type_record.sql` 回归测试覆盖以下场景: + +* 包规范、包体和匿名块中的记录类型声明; +* 非限定、自身包限定和跨包类型引用; +* 局部变量、包变量、返回值以及 `IN`、`OUT` 参数; +* 无列定义列表的复合结果查询; +* 标量字段、命名复合字段和嵌套包记录字段; +* `NOT NULL`、`DEFAULT` 和 `TABLE OF` 的错误处理; +* 类型名与记录变量混淆、字段命名空间隔离和重复声明检查; +* 编译错误后的会话与包缓存可用性。 diff --git a/CN/modules/ROOT/pages/master/oracle_compatibility/package_record_type.adoc b/CN/modules/ROOT/pages/master/oracle_compatibility/package_record_type.adoc new file mode 100644 index 00000000..5ac3cf3c --- /dev/null +++ b/CN/modules/ROOT/pages/master/oracle_compatibility/package_record_type.adoc @@ -0,0 +1,287 @@ +:sectnums: +:sectnumlevels: 5 + += 包中的 RECORD 类型声明 + +== 概述 + +IvorySQL 的 PL/iSQL 支持使用 `TYPE ... IS RECORD` 声明复合记录类型。记录类型可以声明在包规范、包体或匿名块中,并可用于变量、函数返回值以及 `IN`、`OUT` 参数。 + +在包规范中声明的记录类型是包的公共成员,可通过 `包名.类型名` 从其他包或匿名块中引用;在包体中声明的记录类型仅供该包体内部使用。 + +== 语法 + +[source,sql] +---- +TYPE type_name IS RECORD ( + field_name datatype + [, field_name datatype ...] +); +---- + +=== 参数 + +[cols="1,4",options="header"] +|=== +|参数 |说明 +|`type_name` |记录类型的名称。 +|`field_name` |记录字段的名称。字段名称仅在当前记录类型中有效。 +|`datatype` |字段的数据类型,可以是标量类型、命名复合类型或另一个包记录类型。 +|=== + +== 使用示例 + +=== 在包规范中声明记录类型 + +以下示例声明公共记录类型 `employee_t`,并将其用作函数返回类型: + +[source,sql] +---- +CREATE OR REPLACE PACKAGE employee_pkg AS + TYPE employee_t IS RECORD ( + id NUMBER, + name VARCHAR2(50), + salary FLOAT + ); + + FUNCTION make_employee( + p_id NUMBER, + p_name VARCHAR2, + p_salary FLOAT + ) RETURN employee_t; +END employee_pkg; +/ + +CREATE OR REPLACE PACKAGE BODY employee_pkg AS + FUNCTION make_employee( + p_id NUMBER, + p_name VARCHAR2, + p_salary FLOAT + ) RETURN employee_t IS + result employee_t; + BEGIN + result.id := p_id; + result.name := p_name; + result.salary := p_salary; + RETURN result; + END; +END employee_pkg; +/ + +SELECT * FROM employee_pkg.make_employee(1, 'Alice', 50000); +---- + +查询复合返回值时不需要提供列定义列表,返回结果的字段名称和类型来自 `employee_t`。 + +=== 使用包限定类型名 + +在声明类型的包内,可以使用非限定类型名 `employee_t`,也可以使用完整名称 `employee_pkg.employee_t`。在包外引用时必须使用包限定名称: + +[source,sql] +---- +DECLARE + employee employee_pkg.employee_t; +BEGIN + employee.id := 2; + employee.name := 'Bob'; + employee.salary := 60000; + + RAISE NOTICE 'id=%, name=%, salary=%', + employee.id, employee.name, employee.salary; +END; +/ +---- + +=== 跨包引用记录类型 + +可以建立专门保存公共类型的包,其他包通过限定名称引用其中的记录类型: + +[source,sql] +---- +CREATE OR REPLACE PACKAGE common_types AS + TYPE address_t IS RECORD ( + city VARCHAR2(50), + street VARCHAR2(100) + ); +END common_types; +/ + +CREATE OR REPLACE PACKAGE customer_pkg AS + FUNCTION make_address( + p_city VARCHAR2, + p_street VARCHAR2 + ) RETURN common_types.address_t; +END customer_pkg; +/ + +CREATE OR REPLACE PACKAGE BODY customer_pkg AS + FUNCTION make_address( + p_city VARCHAR2, + p_street VARCHAR2 + ) RETURN common_types.address_t IS + result common_types.address_t; + BEGIN + result.city := p_city; + result.street := p_street; + RETURN result; + END; +END customer_pkg; +/ + +SELECT * FROM customer_pkg.make_address('Beijing', 'Chang''an Avenue'); +---- + +=== 用作 IN 和 OUT 参数 + +包记录类型可直接用于子程序参数: + +[source,sql] +---- +CREATE OR REPLACE PACKAGE record_parameter_pkg AS + TYPE person_t IS RECORD ( + id NUMBER, + name VARCHAR2(50) + ); + + FUNCTION describe(p_person IN person_t) RETURN VARCHAR2; + PROCEDURE fill_person( + p_id NUMBER, + p_name VARCHAR2, + p_person OUT person_t + ); +END record_parameter_pkg; +/ + +CREATE OR REPLACE PACKAGE BODY record_parameter_pkg AS + FUNCTION describe(p_person IN person_t) RETURN VARCHAR2 IS + BEGIN + RETURN 'id=' || p_person.id || ', name=' || p_person.name; + END; + + PROCEDURE fill_person( + p_id NUMBER, + p_name VARCHAR2, + p_person OUT person_t + ) IS + BEGIN + p_person.id := p_id; + p_person.name := p_name; + END; +END record_parameter_pkg; +/ + +DECLARE + person record_parameter_pkg.person_t; +BEGIN + record_parameter_pkg.fill_person(10, 'Carol', person); + RAISE NOTICE '%', record_parameter_pkg.describe(person); +END; +/ +---- + +=== 在包体中声明私有记录类型 + +记录类型也可以只在包体中声明。此类类型不能被包外代码引用,适合保存实现细节: + +[source,sql] +---- +CREATE OR REPLACE PACKAGE private_record_pkg AS + FUNCTION format_value(p_id NUMBER, p_text VARCHAR2) RETURN VARCHAR2; +END private_record_pkg; +/ + +CREATE OR REPLACE PACKAGE BODY private_record_pkg AS + TYPE value_t IS RECORD ( + id NUMBER, + text_value VARCHAR2(50) + ); + + FUNCTION format_value(p_id NUMBER, p_text VARCHAR2) RETURN VARCHAR2 IS + value value_t; + BEGIN + value.id := p_id; + value.text_value := p_text; + RETURN value.id || ': ' || value.text_value; + END; +END private_record_pkg; +/ +---- + +=== 在匿名块中声明记录类型 + +[source,sql] +---- +DECLARE + TYPE point_t IS RECORD ( + x NUMBER, + y NUMBER + ); + point point_t; +BEGIN + point.x := 10; + point.y := 20; + RAISE NOTICE 'x=%, y=%', point.x, point.y; +END; +/ +---- + +== 嵌套复合字段 + +记录字段可以使用命名复合类型或其他包记录类型。 + +对于通过 `CREATE TYPE ... AS` 创建的命名复合类型,支持逐字段写入和嵌套读取: + +[source,sql] +---- +CREATE TYPE address_t AS ( + city VARCHAR2(50), + street VARCHAR2(100) +); + +CREATE OR REPLACE PACKAGE nested_record_pkg AS + TYPE person_t IS RECORD ( + name VARCHAR2(50), + address address_t + ); + + FUNCTION make_person RETURN VARCHAR2; +END nested_record_pkg; +/ + +CREATE OR REPLACE PACKAGE BODY nested_record_pkg AS + FUNCTION make_person RETURN VARCHAR2 IS + person person_t; + BEGIN + person.name := 'Alice'; + person.address.city := 'Beijing'; + person.address.street := 'Chang''an Avenue'; + RETURN person.name || ', ' || person.address.city; + END; +END nested_record_pkg; +/ +---- + +== 限制 + +[cols="2,4",options="header"] +|=== +|限制 |说明 +|字段约束和默认值 |记录字段暂不支持 `NOT NULL` 和 `DEFAULT`,使用时会在编译阶段报错。 +|集合类型 |`TYPE ... IS TABLE OF` 暂未实现,使用时会返回不支持错误。 +|嵌套包记录的逐字段写入 |当外层记录的字段本身是另一个包记录类型时,可以读取嵌套字段,也可以对整个字段赋值,但暂不支持 `outer.inner.field := value` 形式的逐字段写入。 +|类型名称的使用 |记录类型名不是记录变量,不能通过 `type_name.field` 读取字段,也不能对类型名本身使用 `%TYPE`。 +|=== + +嵌套包记录类型可以先写入内层变量,再对整个字段赋值: + +[source,sql] +---- +-- outer_value.inner_value 是另一个包记录类型 +inner_value.x := 10; +inner_value.y := 'nested'; +outer_value.inner_value := inner_value; -- 支持 + +RAISE NOTICE '%', outer_value.inner_value.x; -- 支持嵌套读取 + +-- outer_value.inner_value.x := 10; -- 暂不支持 +---- diff --git a/EN/modules/ROOT/nav.adoc b/EN/modules/ROOT/nav.adoc index 594bda81..9709a3e9 100644 --- a/EN/modules/ROOT/nav.adoc +++ b/EN/modules/ROOT/nav.adoc @@ -32,6 +32,7 @@ ** 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/package_record_type.adoc[27、RECORD type declarations in packages] * 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/package_record_type.adoc[Package RECORD type] ** 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/package_record_type.adoc b/EN/modules/ROOT/pages/master/compatibility_features_design/package_record_type.adoc new file mode 100644 index 00000000..8cd7811b --- /dev/null +++ b/EN/modules/ROOT/pages/master/compatibility_features_design/package_record_type.adoc @@ -0,0 +1,130 @@ +:sectnums: +:sectnumlevels: 5 + += Package RECORD Type Design + +== Background + +=== Oracle semantics + +Oracle PL/SQL allows `TYPE ... IS RECORD` to define a record composed of multiple fields. A record type can be declared in a package specification, package body, or PL/SQL block, then used as a variable, parameter, or function return type. A type declared in a package specification can be referenced by other program units through a package-qualified name. + +=== Purpose + +Shared type packages and package interfaces that accept or return records are common in Oracle applications. Package record types preserve these interfaces and field access patterns when applications are migrated to IvorySQL, without splitting records into scalar parameters or creating separate catalog composite types. + +== Architecture + +=== Design choice + +When the parser reads `TYPE ... IS RECORD`, PL/iSQL builds a `TupleDesc` from the field list, finalizes it, and calls `BlessTupleDesc()`. This gives the anonymous `RECORD` a resolvable typmod. The resulting type retains its complete field structure during compilation and can be used for variables, subprogram parameters, and return types. + +Type declarations and record variables use different namespace item types: + +[cols="2,2,4",options="header"] +|=== +|Object |Namespace type |Purpose +|Record type declaration |`PLISQL_NSTYPE_ROWTYPE` |Stores the type's field list and tuple descriptor. +|Record variable |`PLISQL_NSTYPE_REC` |Stores the runtime record value and field access state. +|=== + +The parser can therefore distinguish a type name from a variable name. Invalid expressions such as `type_name.field` and `type_name%TYPE` return compilation errors instead of accessing an incompatible data structure. + +=== Visibility and type resolution + +* A record type declared in a package specification is a public package member and is resolved through `package_name.type_name`. +* A type declared in a package body is added only to the package body's private namespace. +* Code inside the declaring package can use either an unqualified type name or the package-qualified name. +* Cross-package references look up the type in the target package's compiled type information and preserve its typmod and memory-context lifetime. +* Type names participate in duplicate-declaration checks and cannot conflict with another type or variable in the same scope. + +=== Code organization + +[source,text] +---- +src/pl/plisql/src/ +├── pl_gram.y -- TYPE ... IS RECORD grammar and field checks +├── pl_comp.c -- type lookup, column references, nested fields +├── pl_package.c -- package type registration and qualified lookup +├── pl_subproc_function.c -- subprogram parameter and return types +├── pl_exec.c -- record instantiation and TupleDesc lifetime +├── plisql.h -- namespace item definitions +├── pl_unreserved_kwlist.h -- related unreserved keywords +├── sql/ +│ └── plisql_package_type_record.sql -- regression test SQL +└── expected/ + └── plisql_package_type_record.out -- expected output +---- + +== Implementation Details + +=== Declaration phase + +The grammar reads the field names and data types, then creates a `PLiSQL_row` for the declaration. Field parsing uses a private temporary namespace, so field names do not leak into the surrounding package or block and do not create false conflicts with local variables. + +After the field list is complete, the system builds and finalizes the `TupleDesc`, then registers it as an anonymous `RECORDOID` with a typmod. A record variable can therefore be instantiated and assigned field by field even before a complete-row assignment has occurred. + +=== Parameters and return values + +Package record types use `RECORDOID`; the positive typmod identifies the concrete structure. Type resolution passes both the OID and typmod through the following contexts: + +* local and package variables; +* `IN` and `OUT` parameters; +* function return types; +* same-package and cross-package qualified references. + +When a function returns a package record type, the planner can recover the composite structure from the typmod. `SELECT * FROM package_name.function_name(...)` therefore needs no additional column definition list. + +=== Nested composite fields + +Field type resolution carries the real type OID and typmod instead of degrading every nested field to an unstructured `RECORDOID/-1`. When `resolve_column_ref()` handles a multi-level field reference, it builds a `FieldSelect`, which supports expressions such as `record.field.subfield`. + +For a named composite type created with `CREATE TYPE ... AS`, `pg_type.typrelid` lets the executor recover the structure, so nested reads and field-by-field writes work. + +For a field whose type is another package record, whole-field assignment and nested reads work, but field-by-field writes are limited by PostgreSQL's core `FieldStore` node. `FieldStore` does not carry a result typmod and looks up the type with typmod `-1` at execution time. It cannot recover an anonymous package record registered under a positive typmod. The current implementation reports a clear error for this case. + +=== Tuple descriptor lifetime + +The `TupleDesc` ownership rules differ for package record types and named composite types: + +* A package record type obtains a reference-counted descriptor through row-type lookup and must release it after use. +* A named composite type may return a descriptor owned by the type cache; callers must not release it. + +The implementation handles ownership according to the descriptor's source, avoiding accidental release of a type-cache descriptor. Data needed by cross-package and self-qualified references is copied into a memory context with a sufficient lifetime, so it remains valid while the package cache is active. + +== Error Handling + +=== Unsupported field clauses + +Record fields do not support `NOT NULL` or `DEFAULT`: + +* `PLiSQL_row` has no storage for per-field default expressions. +* Without a field default, a record instance cannot satisfy a field-level `NOT NULL` constraint. + +The grammar rejects both clauses at compile time. This prevents defaults from being silently ignored and avoids assertion failures when a record variable is instantiated. + +=== TABLE OF declarations + +The grammar recognizes `TYPE name IS TABLE OF ...`, but collection types are not implemented. IvorySQL returns a not-supported error that includes the type name instead of a generic syntax error. + +=== Type and variable confusion + +Because type declarations and record variables use separate namespace item types, the following cases are rejected explicitly: + +* treating a type name as a record value and accessing a field; +* applying `%TYPE` to the type declaration itself; +* appending a nonexistent field or name to a package-qualified type name; +* declaring a duplicate type, or using the same name for a type and a variable in one scope. + +== Test Coverage + +The `plisql_package_type_record.sql` regression test covers: + +* record declarations in package specifications, package bodies, and anonymous blocks; +* unqualified, self-qualified, and cross-package type references; +* local variables, package variables, return values, and `IN` or `OUT` parameters; +* composite result queries without a column definition list; +* scalar fields, named composite fields, and nested package record fields; +* errors for `NOT NULL`, `DEFAULT`, and `TABLE OF`; +* type/record-variable confusion, field namespace isolation, and duplicate declarations; +* session and package-cache usability after compilation errors. diff --git a/EN/modules/ROOT/pages/master/oracle_compatibility/package_record_type.adoc b/EN/modules/ROOT/pages/master/oracle_compatibility/package_record_type.adoc new file mode 100644 index 00000000..dd1f9e6c --- /dev/null +++ b/EN/modules/ROOT/pages/master/oracle_compatibility/package_record_type.adoc @@ -0,0 +1,287 @@ +:sectnums: +:sectnumlevels: 5 + += RECORD Type Declarations in Packages + +== Overview + +IvorySQL PL/iSQL supports composite record types declared with `TYPE ... IS RECORD`. A record type can be declared in a package specification, package body, or anonymous block. It can be used for variables, function return values, and `IN` or `OUT` parameters. + +A type declared in a package specification is a public package member and can be referenced from another package or an anonymous block with `package_name.type_name`. A type declared in a package body is private to that package body. + +== Syntax + +[source,sql] +---- +TYPE type_name IS RECORD ( + field_name datatype + [, field_name datatype ...] +); +---- + +=== Parameters + +[cols="1,4",options="header"] +|=== +|Parameter |Description +|`type_name` |Name of the record type. +|`field_name` |Name of a record field. The name is scoped to the record type declaration. +|`datatype` |Field data type. It can be a scalar type, a named composite type, or another package record type. +|=== + +== Examples + +=== Declaring a record type in a package specification + +The following example declares a public `employee_t` record type and uses it as a function return type: + +[source,sql] +---- +CREATE OR REPLACE PACKAGE employee_pkg AS + TYPE employee_t IS RECORD ( + id NUMBER, + name VARCHAR2(50), + salary FLOAT + ); + + FUNCTION make_employee( + p_id NUMBER, + p_name VARCHAR2, + p_salary FLOAT + ) RETURN employee_t; +END employee_pkg; +/ + +CREATE OR REPLACE PACKAGE BODY employee_pkg AS + FUNCTION make_employee( + p_id NUMBER, + p_name VARCHAR2, + p_salary FLOAT + ) RETURN employee_t IS + result employee_t; + BEGIN + result.id := p_id; + result.name := p_name; + result.salary := p_salary; + RETURN result; + END; +END employee_pkg; +/ + +SELECT * FROM employee_pkg.make_employee(1, 'Alice', 50000); +---- + +The composite result can be queried without a column definition list. Field names and types come from `employee_t`. + +=== Using a package-qualified type name + +Inside the declaring package, both the unqualified name `employee_t` and the qualified name `employee_pkg.employee_t` are valid. Code outside the package uses the qualified name: + +[source,sql] +---- +DECLARE + employee employee_pkg.employee_t; +BEGIN + employee.id := 2; + employee.name := 'Bob'; + employee.salary := 60000; + + RAISE NOTICE 'id=%, name=%, salary=%', + employee.id, employee.name, employee.salary; +END; +/ +---- + +=== Referencing a record type across packages + +A package can hold shared types, which other packages reference by qualified name: + +[source,sql] +---- +CREATE OR REPLACE PACKAGE common_types AS + TYPE address_t IS RECORD ( + city VARCHAR2(50), + street VARCHAR2(100) + ); +END common_types; +/ + +CREATE OR REPLACE PACKAGE customer_pkg AS + FUNCTION make_address( + p_city VARCHAR2, + p_street VARCHAR2 + ) RETURN common_types.address_t; +END customer_pkg; +/ + +CREATE OR REPLACE PACKAGE BODY customer_pkg AS + FUNCTION make_address( + p_city VARCHAR2, + p_street VARCHAR2 + ) RETURN common_types.address_t IS + result common_types.address_t; + BEGIN + result.city := p_city; + result.street := p_street; + RETURN result; + END; +END customer_pkg; +/ + +SELECT * FROM customer_pkg.make_address('Beijing', 'Chang''an Avenue'); +---- + +=== Using a record type for `IN` and `OUT` parameters + +A package record type can be used directly in subprogram parameters: + +[source,sql] +---- +CREATE OR REPLACE PACKAGE record_parameter_pkg AS + TYPE person_t IS RECORD ( + id NUMBER, + name VARCHAR2(50) + ); + + FUNCTION describe(p_person IN person_t) RETURN VARCHAR2; + PROCEDURE fill_person( + p_id NUMBER, + p_name VARCHAR2, + p_person OUT person_t + ); +END record_parameter_pkg; +/ + +CREATE OR REPLACE PACKAGE BODY record_parameter_pkg AS + FUNCTION describe(p_person IN person_t) RETURN VARCHAR2 IS + BEGIN + RETURN 'id=' || p_person.id || ', name=' || p_person.name; + END; + + PROCEDURE fill_person( + p_id NUMBER, + p_name VARCHAR2, + p_person OUT person_t + ) IS + BEGIN + p_person.id := p_id; + p_person.name := p_name; + END; +END record_parameter_pkg; +/ + +DECLARE + person record_parameter_pkg.person_t; +BEGIN + record_parameter_pkg.fill_person(10, 'Carol', person); + RAISE NOTICE '%', record_parameter_pkg.describe(person); +END; +/ +---- + +=== Declaring a private record type in a package body + +A record type can be declared only in a package body. Such a type cannot be referenced outside the package: + +[source,sql] +---- +CREATE OR REPLACE PACKAGE private_record_pkg AS + FUNCTION format_value(p_id NUMBER, p_text VARCHAR2) RETURN VARCHAR2; +END private_record_pkg; +/ + +CREATE OR REPLACE PACKAGE BODY private_record_pkg AS + TYPE value_t IS RECORD ( + id NUMBER, + text_value VARCHAR2(50) + ); + + FUNCTION format_value(p_id NUMBER, p_text VARCHAR2) RETURN VARCHAR2 IS + value value_t; + BEGIN + value.id := p_id; + value.text_value := p_text; + RETURN value.id || ': ' || value.text_value; + END; +END private_record_pkg; +/ +---- + +=== Declaring a record type in an anonymous block + +[source,sql] +---- +DECLARE + TYPE point_t IS RECORD ( + x NUMBER, + y NUMBER + ); + point point_t; +BEGIN + point.x := 10; + point.y := 20; + RAISE NOTICE 'x=%, y=%', point.x, point.y; +END; +/ +---- + +== Nested Composite Fields + +A record field can use a named composite type or another package record type. + +Named composite types created with `CREATE TYPE ... AS` support both field-by-field writes and nested reads: + +[source,sql] +---- +CREATE TYPE address_t AS ( + city VARCHAR2(50), + street VARCHAR2(100) +); + +CREATE OR REPLACE PACKAGE nested_record_pkg AS + TYPE person_t IS RECORD ( + name VARCHAR2(50), + address address_t + ); + + FUNCTION make_person RETURN VARCHAR2; +END nested_record_pkg; +/ + +CREATE OR REPLACE PACKAGE BODY nested_record_pkg AS + FUNCTION make_person RETURN VARCHAR2 IS + person person_t; + BEGIN + person.name := 'Alice'; + person.address.city := 'Beijing'; + person.address.street := 'Chang''an Avenue'; + RETURN person.name || ', ' || person.address.city; + END; +END nested_record_pkg; +/ +---- + +== Limitations + +[cols="2,4",options="header"] +|=== +|Limitation |Description +|Field constraints and defaults |Record fields do not support `NOT NULL` or `DEFAULT`. The declaration is rejected at compile time. +|Collection types |`TYPE ... IS TABLE OF` is parsed but not implemented. The declaration returns a not-supported error. +|Field-by-field writes to nested package records |When an outer record field is another package record type, nested reads and whole-field assignment are supported. `outer.inner.field := value` is not supported. +|Using a type name as a value |A record type name is not a record variable. `type_name.field` cannot read a field, and `%TYPE` cannot be applied to the type declaration itself. +|=== + +For a nested package record, assign the complete inner record instead: + +[source,sql] +---- +-- outer_value.inner_value is another package record type +inner_value.x := 10; +inner_value.y := 'nested'; +outer_value.inner_value := inner_value; -- supported + +RAISE NOTICE '%', outer_value.inner_value.x; -- nested read supported + +-- outer_value.inner_value.x := 10; -- not supported +----