Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions CN/modules/ROOT/nav.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -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部署]
Expand Down Expand Up @@ -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]
Expand Down
Original file line number Diff line number Diff line change
@@ -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` 的错误处理;
* 类型名与记录变量混淆、字段命名空间隔离和重复声明检查;
* 编译错误后的会话与包缓存可用性。
Loading
Loading