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
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
:sectnums:
:sectnumlevels: 5

= DBMS_TRANSACTION 包设计

== 目的

通过 `ivorysql_ora` 扩展提供 Oracle 风格的事务控制接口,复用已有 PL/iSQL 语句和事务管理函数。本实现支持 `COMMIT`、`ROLLBACK`、`SAVEPOINT`、`ROLLBACK_SAVEPOINT`、`READ_ONLY` 和 `READ_WRITE`,改动限定在扩展内部,不增加内核事务接口。

使用方式及调用限制见 xref:master/oracle_compatibility/compat_dbms_transaction.adoc[DBMS_TRANSACTION 包]。

== 实现说明

=== 代码组织

[cols="3,4",options="header"]
|===
|文件 |职责
|`src/builtin_packages/dbms_transaction/dbms_transaction--1.0.sql` |注册 C 函数,定义包规范与包体,授予包执行权限
|`src/builtin_packages/dbms_transaction/dbms_transaction.c` |保存点名称校验、调用上下文检查及事务函数桥接
|`sql/dbms_transaction.sql` |事务操作、异常输入、自治事务及跨用户调用的回归用例
|`expected/dbms_transaction.out`、`expected/dbms_transaction_1.out` |没有或具备可用 dblink 环境时的期望结果
|`Makefile`、`meson.build`、`ivorysql_ora_merge_sqls` |编译、回归及扩展 SQL 合并注册
|===

上述路径均相对于 `contrib/ivorysql_ora/`。

=== 包接口与权限

包声明使用 `AUTHID CURRENT_USER`,并执行 `GRANT EXECUTE ON PACKAGE dbms_transaction TO PUBLIC`,使普通用户能够以自身权限调用。包级执行授权不改变业务对象的访问权限。

两个保存点 C 函数注册在 `sys` 模式中,均为 `VOLATILE`,且未声明 `STRICT`,以便对 `NULL` 名称给出明确错误。

[source,text]
----
顶层 CALL dbms_transaction.savepoint('sp1')
→ PL/iSQL 包体
→ sys.ora_dbms_transaction_savepoint(text)
→ DefineSavepoint(name)
→ 顶层命令结束时完成子事务状态转换
----

=== 子程序映射

[cols="2,4",options="header"]
|===
|子程序 |实现
|`COMMIT` |包体执行 PL/iSQL 原生 `COMMIT` 语句
|`ROLLBACK` |包体执行 PL/iSQL 原生 `ROLLBACK` 语句
|`SAVEPOINT` |包体通过 `PERFORM` 调用 C 函数,最终调用 `DefineSavepoint()`
|`ROLLBACK_SAVEPOINT` |包体通过 `PERFORM` 调用 C 函数,最终调用 `RollbackToSavepoint()`
|`READ_ONLY` |动态执行 `SET TRANSACTION READ ONLY`
|`READ_WRITE` |动态执行 `SET TRANSACTION READ WRITE`
|===

`COMMIT` 和 `ROLLBACK` 复用 PL/iSQL 的事务终止路径,继承其原子上下文限制。包没有额外开启事务,也不会让原本不允许终止事务的调用上下文获得提交或回滚能力。

读写模式过程只设置访问模式,不改变隔离级别,也不实现额外的 Oracle 快照机制。切换模式的时机和写入限制由底层 `SET TRANSACTION` 规则处理。

=== 保存点桥接

PL/iSQL 没有原生保存点语句,因此两个包过程由 C 函数桥接到 `access/xact.h` 声明的事务管理函数。它们按以下顺序执行:

. `get_savepoint_name()` 拒绝 `NULL` 名称和长度达到 256 字节的名称;上限为 255 字节,名称按字符串传递。
. `RequireTransactionBlock(true, ...)` 确认存在显式事务块,防止在无效状态下调用保存点函数。
. `check_not_nested_in_pl_call()` 通过 `SPI_get_connected() > 0` 判断是否嵌套在其他 PL/iSQL 调用中;若嵌套则报错。
. 调用 `DefineSavepoint()` 或 `RollbackToSavepoint()`,由内核完成后续事务状态转换。

保存点函数只标记待处理的事务状态,实际子事务启动或回滚在顶层命令结束后完成。如果嵌套过程在该转换完成前继续执行,可能访问尚未就绪的事务状态。因此保存点操作必须作为独立顶层 `CALL`,不能在 C 桥接中强行提交命令来绕过限制。

这里直接调用事务管理函数,没有新增 `SPI_savepoint` 等 SPI 接口。`SPI_get_connected()` 仅用于检查调用深度,不负责执行保存点操作。

=== 边界与错误处理

[cols="3,2,4",options="header"]
|===
|条件 |SQLSTATE |诊断
|保存点名称为 `NULL` |`22004` |`DBMS_TRANSACTION savepoint name must not be NULL`
|保存点名称超过 255 字节 |`22023` |`DBMS_TRANSACTION savepoint name too long (max 255 bytes)`
|保存点操作不在显式事务块中 |`25P01` |只能用于事务块的错误
|保存点操作嵌套在其他 PL/iSQL 调用中 |`25000` |必须移到独立顶层 `CALL` 的错误及提示
|回滚到不存在的保存点 |`3B001` |内核返回 `savepoint ... does not exist`
|原子上下文中调用包提交或回滚 |`2D000` |`invalid transaction termination`
|只读事务取得快照后切换为读写模式 |`25001` |`transaction read-write mode must be set before any query`
|===

同名保存点重定义、重复回滚及回滚后撤销后续保存点等行为直接复用内核语义。Oracle 空字符串转 `NULL` 的处理发生在进入 C 桥接之前。

=== 自治事务

保存点嵌套检查不豁免自治事务,也不依据用户可设置的 `plisql.inside_autonomous_transaction` 参数放行。真正的自治事务在独立 dblink 会话执行,当前回归场景没有显式事务块,因此先被 `RequireTransactionBlock()` 拒绝。

包内提交和回滚仍受事务终止上下文约束;当前自治事务用例返回 `invalid transaction termination`。读写模式过程可设置独立会话的事务模式,不影响父事务的访问模式。

== 测试验证

`dbms_transaction` 回归组覆盖提交与回滚、自动提交开关、匿名块调用、保存点回滚与重定义、具名参数、名称边界、读写模式切换、原子上下文、保存点嵌套防护及调用者权限。

自治事务用例先探测 dblink 回环连接;可用时验证自治事务中的事务控制与访问模式,否则跳过该部分并使用另一份期望输出。

在已配置、编译并安装的 IvorySQL 源码目录中,可运行 `make -C contrib/ivorysql_ora oracle-installcheck ORA_REGRESS=dbms_transaction`。目标实例需具备对应版本的 `ivorysql_ora`,自治事务测试还需可用的 dblink 环境。
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
:sectnums:
:sectnumlevels: 5

= DBMS_TRANSACTION 包

== 目的

`DBMS_TRANSACTION` 提供提交、回滚、保存点及事务读写模式控制接口,便于迁移使用 Oracle 事务控制包的应用。IvorySQL 当前实现六个过程,执行行为受 PL/iSQL 和底层事务机制的约束。

包以调用者权限运行,并向 `PUBLIC` 授予执行权限。访问业务表仍需调用者具备相应权限。

== 功能说明

=== 支持的接口

[cols="3,4",options="header"]
|===
|接口 |功能
|`COMMIT` |提交当前事务
|`ROLLBACK` |回滚当前事务
|`SAVEPOINT(savept IN VARCHAR2)` |在当前事务中建立命名保存点
|`ROLLBACK_SAVEPOINT(savept IN VARCHAR2)` |回滚到指定保存点,不结束当前事务
|`READ_ONLY` |将当前事务设置为只读
|`READ_WRITE` |将当前事务设置为读写
|===

SQL 中使用 `CALL dbms_transaction.过程名(...)`;PL/iSQL 块内可直接调用包过程。保存点过程支持具名参数,例如 `savept => 'sp1'`。

=== 调用限制

* `COMMIT` 和 `ROLLBACK` 必须在允许事务终止的非原子执行上下文中调用。显式 SQL 事务块内或 psql 关闭自动提交后调用,会报 `invalid transaction termination`。顶层的非原子 PL/iSQL 匿名块可以调用,见下面的提交与回滚示例。
* `SAVEPOINT` 和 `ROLLBACK_SAVEPOINT` 必须在显式事务块内,通过独立的顶层 `CALL` 语句执行。不能放到另一个 PL/iSQL 过程、函数或匿名块内部。
* 保存点名称最多 255 字节,按字符串处理,可以包含空格。`NULL` 名称被拒绝;按 Oracle 语义转换为 `NULL` 的空字符串也被拒绝。
* 同名保存点重复定义时,回滚定位到最近一次定义。回滚后目标保存点仍可使用,但其后建立的保存点被撤销。不存在的保存点会报错。
* 建议在事务开始后、访问业务表之前设置读写模式。同一事务中先调用 `READ_ONLY` 再调用 `READ_WRITE` 会报 `transaction read-write mode must be set before any query`;反向切换允许。

`READ_ONLY` 设置访问模式,不自动改变隔离级别。事务中的快照和读一致性仍由 IvorySQL 的隔离级别决定。

== 使用示例

以下示例在 Oracle 兼容模式的 psql 中执行。保持自动提交开启,提交与回滚示例中的 PL/iSQL 匿名块不要再放入外层 SQL `BEGIN;` 事务块。

=== 提交与回滚

[source,sql]
----
\set AUTOCOMMIT on
CREATE TABLE dbms_tx_demo (id int, val varchar2(100));

BEGIN
INSERT INTO dbms_tx_demo VALUES (1, 'committed');
dbms_transaction.commit();
INSERT INTO dbms_tx_demo VALUES (2, 'rolled back');
dbms_transaction.rollback();
END;
/

SELECT * FROM dbms_tx_demo ORDER BY id;
----

查询只返回 `(1, 'committed')`。第一个插入已提交,第二个插入被回滚。若在自动提交模式下把 `INSERT` 和 `CALL dbms_transaction.rollback()` 分成两条顶层语句,前一条插入已经提交,后续回滚不会撤销它。

=== 保存点回滚

[source,sql]
----
BEGIN;
INSERT INTO dbms_tx_demo VALUES (3, 'before savepoint');
CALL dbms_transaction.savepoint(savept => 'sp1');
INSERT INTO dbms_tx_demo VALUES (4, 'after savepoint');
CALL dbms_transaction.rollback_savepoint(savept => 'sp1');
COMMIT;

SELECT * FROM dbms_tx_demo WHERE id IN (3, 4) ORDER BY id;
----

查询只返回 `(3, 'before savepoint')`。这里的 `BEGIN;` 和 `COMMIT;` 是 SQL 事务语句,保存点操作各自通过独立的 `CALL` 执行。结束这个显式事务块应使用 SQL `COMMIT;` 或 `ROLLBACK;`。

=== 读写模式

[source,sql]
----
BEGIN;
CALL dbms_transaction.read_write();
INSERT INTO dbms_tx_demo VALUES (5, 'read write');
COMMIT;

BEGIN;
CALL dbms_transaction.read_only();
INSERT INTO dbms_tx_demo VALUES (6, 'read only');
-- ERROR: cannot execute INSERT in a read-only transaction
ROLLBACK;

DROP TABLE dbms_tx_demo;
----

读写事务中的第 5 行插入成功;只读事务禁止对该普通表执行插入,第 6 行不会保存。

== 兼容性说明

`PRAGMA AUTONOMOUS_TRANSACTION` 不豁免上述事务上下文限制。当前自治事务回归场景中,保存点调用因没有显式事务块而报错,包内提交与回滚调用报 `invalid transaction termination`;`READ_ONLY` 和 `READ_WRITE` 可以设置自治事务自身的访问模式。
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
:sectnums:
:sectnumlevels: 5

= DBMS_TRANSACTION Package Design

== Purpose

Provide Oracle-style transaction control through the `ivorysql_ora` extension by reusing existing PL/iSQL statements and transaction management functions. The implementation supports `COMMIT`, `ROLLBACK`, `SAVEPOINT`, `ROLLBACK_SAVEPOINT`, `READ_ONLY`, and `READ_WRITE`. Changes stay within the extension and add no core transaction interfaces.

See xref:master/oracle_compatibility/compat_dbms_transaction.adoc[DBMS_TRANSACTION Package] for usage and call restrictions.

== Implementation

=== Code organization

[cols="3,4",options="header"]
|===
|File |Responsibility
|`src/builtin_packages/dbms_transaction/dbms_transaction--1.0.sql` |Registers C functions, defines the package specification and body, and grants package execution
|`src/builtin_packages/dbms_transaction/dbms_transaction.c` |Validates savepoint names and call contexts, and bridges to transaction functions
|`sql/dbms_transaction.sql` |Regression cases for transaction operations, invalid input, autonomous transactions, and cross-user execution
|`expected/dbms_transaction.out`, `expected/dbms_transaction_1.out` |Expected results without and with a usable dblink environment, respectively
|`Makefile`, `meson.build`, `ivorysql_ora_merge_sqls` |Build, regression, and extension SQL merge registration
|===

These paths are relative to `contrib/ivorysql_ora/`.

=== Package interface and privileges

The package declares `AUTHID CURRENT_USER` and executes `GRANT EXECUTE ON PACKAGE dbms_transaction TO PUBLIC`, allowing ordinary users to call it with their own privileges. Package execution privileges do not change access rights on application objects.

The two savepoint C functions are registered in the `sys` schema as `VOLATILE`. Neither is declared `STRICT`, so the implementation can report an explicit error for a NULL name.

[source,text]
----
Top-level CALL dbms_transaction.savepoint('sp1')
→ PL/iSQL package body
→ sys.ora_dbms_transaction_savepoint(text)
→ DefineSavepoint(name)
→ Subtransaction state transition completes at command end
----

=== Subprogram mappings

[cols="2,4",options="header"]
|===
|Subprogram |Implementation
|`COMMIT` |Executes the native PL/iSQL `COMMIT` statement in the package body
|`ROLLBACK` |Executes the native PL/iSQL `ROLLBACK` statement in the package body
|`SAVEPOINT` |Uses `PERFORM` to call the C function, which calls `DefineSavepoint()`
|`ROLLBACK_SAVEPOINT` |Uses `PERFORM` to call the C function, which calls `RollbackToSavepoint()`
|`READ_ONLY` |Dynamically executes `SET TRANSACTION READ ONLY`
|`READ_WRITE` |Dynamically executes `SET TRANSACTION READ WRITE`
|===

`COMMIT` and `ROLLBACK` reuse PL/iSQL's transaction termination path and inherit its atomic-context restrictions. The package does not open an additional transaction or enable commit and rollback in contexts that prohibit transaction termination.

The access-mode procedures set the mode without changing the isolation level or adding an Oracle snapshot mechanism. Timing and write restrictions follow the underlying `SET TRANSACTION` rules.

=== Savepoint bridge

PL/iSQL has no native savepoint statement, so two C functions bridge the package procedures to transaction management functions declared in `access/xact.h`. They execute these steps in order:

. `get_savepoint_name()` rejects NULL names and names of 256 bytes or more. The limit is 255 bytes, and names are passed as strings.
. `RequireTransactionBlock(true, ...)` verifies that an explicit transaction block exists, preventing savepoint operations in an invalid state.
. `check_not_nested_in_pl_call()` checks `SPI_get_connected() > 0` to detect nesting inside another PL/iSQL invocation and raises an error if nested.
. `DefineSavepoint()` or `RollbackToSavepoint()` is called, leaving subsequent transaction state transitions to the core.

The savepoint functions mark pending transaction states. Subtransaction startup or rollback completes after the top-level command ends. A nested routine continuing before that transition could access a transaction state that is not ready. Savepoint operations therefore require separate top-level `CALL` statements; forcibly committing the command inside the C bridge cannot safely bypass this restriction.

The bridge calls transaction management functions directly and adds no SPI interfaces such as `SPI_savepoint`. `SPI_get_connected()` only checks invocation depth; it does not execute the savepoint operation.

=== Boundaries and errors

[cols="3,2,4",options="header"]
|===
|Condition |SQLSTATE |Diagnostic
|NULL savepoint name |`22004` |`DBMS_TRANSACTION savepoint name must not be NULL`
|Savepoint name exceeds 255 bytes |`22023` |`DBMS_TRANSACTION savepoint name too long (max 255 bytes)`
|Savepoint operation outside an explicit transaction block |`25P01` |Error requiring a transaction block
|Savepoint operation nested inside another PL/iSQL call |`25000` |Error and hint requiring a separate top-level `CALL`
|Rollback to a nonexistent savepoint |`3B001` |Core error: `savepoint ... does not exist`
|Package commit or rollback in an atomic context |`2D000` |`invalid transaction termination`
|Switch from read-only to read-write after a snapshot |`25001` |`transaction read-write mode must be set before any query`
|===

Redefining savepoints, repeated rollback, and removing later savepoints on rollback use core semantics directly. Oracle empty-string-to-NULL handling takes effect before entering the C bridge.

=== Autonomous transactions

The nested savepoint check does not exempt autonomous transactions and does not bypass the guard based on the user-settable `plisql.inside_autonomous_transaction` parameter. Genuine autonomous transactions execute in a separate dblink session. The current regression scenarios have no explicit transaction block, so `RequireTransactionBlock()` rejects them first.

Package commit and rollback calls remain subject to transaction termination constraints; the current autonomous cases return `invalid transaction termination`. The access-mode procedures can set the independent session's transaction mode without changing the parent transaction's mode.

== Verification

The `dbms_transaction` regression group covers commit and rollback, autocommit settings, anonymous block calls, savepoint rollback and redefinition, named arguments, name boundaries, access-mode switching, atomic contexts, nested savepoint guards, and invoker privileges.

Autonomous transaction tests first probe a dblink loopback connection. If available, they verify transaction control and access modes in autonomous transactions. Otherwise, those cases are skipped and the alternative expected output applies.

From a configured, built, and installed IvorySQL source tree, run `make -C contrib/ivorysql_ora oracle-installcheck ORA_REGRESS=dbms_transaction`. The target instance needs the matching version of `ivorysql_ora`; autonomous transaction tests also require a usable dblink environment.
Loading
Loading