Skip to content

Commit fd76c9c

Browse files
Added support for getting the value of ltxid in thin mode.
1 parent e0f480e commit fd76c9c

File tree

11 files changed

+359
-29
lines changed

11 files changed

+359
-29
lines changed

doc/src/api_manual/async_connection.rst

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -523,8 +523,7 @@ AsyncConnection Attributes
523523

524524
.. note:
525525
526-
This attribute is only available when Oracle Database 12.1 or later is
527-
in use
526+
This attribute is only available with Oracle Database 12.1 or later.
528527
529528
.. attribute:: AsyncConnection.max_identifier_length
530529

doc/src/api_manual/connection.rst

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -774,8 +774,8 @@ Connection Attributes
774774
.. note:
775775
776776
This attribute is an extension to the DB API definition. It is only
777-
available when Oracle Database 12.1 or higher is in use on both the
778-
server and the client.
777+
available with Oracle Database 12.1 or higher. In python-oracledb Thick
778+
mode, it also requires Oracle Client libraries 12.1 or higer.
779779
780780
.. attribute:: Connection.max_identifier_length
781781

doc/src/release_notes.rst

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,8 @@ Thin Mode Changes
3333
#) The thread that closes connection pools on interpreter shutdown is now only
3434
started when the first pool is created and not at module import
3535
(`issue 426 <https://github.com/oracle/python-oracledb/issues/426>`__).
36+
#) Added support for Transaction Guard by adding support to get the value of
37+
:attr:`Connection.ltxid`.
3638
#) Fixed hang when attempting to use pipelining against a database that
3739
doesn't support the end of response flag.
3840
#) Fixed hang when using asyncio and a connection is unexpectedly closed by

doc/src/user_guide/appendix_a.rst

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -287,7 +287,7 @@ see :ref:`driverdiff` and :ref:`compatibility`.
287287
- Yes - no callback
288288
- Yes - no callback
289289
* - Transaction Guard (TG) (see :ref:`tg`)
290-
- No
290+
- Yes
291291
- Yes
292292
- Yes
293293
* - Data Guard (DG) and Active Data Guard (ADG)

doc/src/user_guide/ha.rst

Lines changed: 25 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -161,13 +161,9 @@ Python-oracledb supports `Transaction Guard
161161
<https://www.oracle.com/pls/topic/lookup?ctx=dblatest&
162162
id=GUID-A675AF7B-6FF0-460D-A6E6-C15E7C328C8F>`__ which enables Python
163163
application to verify the success or failure of the last transaction in the
164-
event of an unplanned outage. This feature is available when both client and
165-
database are 12.1 or higher.
166-
167-
.. note::
168-
169-
The Transaction Guard feature is only supported in the python-oracledb
170-
Thick mode. See :ref:`enablingthick`.
164+
event of an unplanned outage. This feature requires Oracle Database 12.1 or
165+
higher. When using python-oracledb Thick mode, Oracle Client 12.1 or higher is
166+
additionally required.
171167

172168
Using Transaction Guard helps to:
173169

@@ -184,7 +180,10 @@ logical transaction id (``ltxid``) from the connection and then call a
184180
procedure to determine the outcome of the commit for this logical transaction
185181
id.
186182

187-
Follow the steps below to use the Transaction Guard feature in Python:
183+
The steps below show how to use Transaction Guard in python-oracledb in a
184+
single-instance database. Refer to Oracle documentation if you are using `RAC
185+
<https://www.oracle.com/pls/ topic/lookup?ctx=dblatest&id=RACAD>`__ or standby
186+
databases.
188187

189188
1. Grant execute privileges to the database users who will be checking the
190189
outcome of the commit. Log in as SYSDBA and run the following command:
@@ -193,8 +192,9 @@ Follow the steps below to use the Transaction Guard feature in Python:
193192
194193
GRANT EXECUTE ON DBMS_APP_CONT TO <username>;
195194
196-
2. Create a new service by executing the following PL/SQL block as SYSDBA.
197-
Replace the ``<service-name>``, ``<network-name>`` and
195+
2. Create a new service by calling `DBMS_SERVICE.CREATE_SERVICE
196+
<https://www.oracle.com/pls/topic/lookup?ctx=dblatest&id=GUID-386E183E-D83C-48A7-8BA3-40248CFB89F4>`__
197+
as SYSDBA. Replace the ``<service-name>``, ``<network-name>`` and
198198
``<retention-value>`` values with suitable values. It is important that the
199199
``COMMIT_OUTCOME`` parameter be set to true for Transaction Guard to
200200
function properly.
@@ -210,12 +210,14 @@ Follow the steps below to use the Transaction Guard feature in Python:
210210
END;
211211
/
212212
213-
3. Start the service by executing the following PL/SQL block as SYSDBA:
213+
3. Start the service by calling `DBMS_SERVICE.START_SERVICE
214+
<https://www.oracle.com/pls/topic/lookup?ctx=dblatest&id=GUID-140B93AC-9021-4091-B797-7CA3AAB446FE>`__
215+
as SYSDBA:
214216

215217
.. code-block:: sql
216218
217219
BEGIN
218-
DBMS_SERVICE.start_service('<service-name>');
220+
DBMS_SERVICE.START_SERVICE('<service-name>');
219221
END;
220222
/
221223
@@ -231,12 +233,18 @@ query:
231233

232234
In the Python application code:
233235

234-
* Use the connection attribute :attr:`~Connection.ltxid` to determine the
236+
* Connect to the appropriately enabled database service. If the connection is
237+
TAF, AC or TAC enabled, then do not proceed with TG.
238+
* Check :attr:`oracledb._Error.isrecoverable` to confirm the error is
239+
recoverable. If not, do not proceed with TG.
240+
* Use the connection attribute :attr:`Connection.ltxid` to find the
235241
logical transaction id.
236-
* Call the ``DBMS_APP_CONT.GET_LTXID_OUTCOME`` PL/SQL procedure with the
237-
logical transaction id acquired from the connection attribute. This returns
238-
a boolean value indicating if the last transaction was committed and whether
239-
the last call was completed successfully or not.
242+
* Call the `DBMS_APP_CONT.GET_LTXID_OUTCOME
243+
<https://www.oracle.com/pls/topic/lookup?ctx=dblatest&id=GUID-03CEB530-D3A5-40B1-87C8-5BF1BB5D5D54>`__
244+
PL/SQL procedure with the logical transaction id. This returns a boolean
245+
value indicating if the last transaction was committed and whether the last
246+
call was completed successfully or not.
247+
* Take any necessary action to re-do uncommitted work.
240248

241249
See the `Transaction Guard Sample
242250
<https://github.com/oracle/python-oracledb/blob/main/

samples/transaction_guard.py

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
# -----------------------------------------------------------------------------
2-
# Copyright (c) 2016, 2023, Oracle and/or its affiliates.
2+
# Copyright (c) 2016, 2025, Oracle and/or its affiliates.
33
#
44
# Portions Copyright 2007-2015, Anthony Tuininga. All rights reserved.
55
#
@@ -57,8 +57,9 @@
5757
import oracledb
5858
import sample_env
5959

60-
# this script is currently only supported in python-oracledb thick mode
61-
oracledb.init_oracle_client(lib_dir=sample_env.get_oracle_client())
60+
# determine whether to use python-oracledb thin mode or thick mode
61+
if not sample_env.get_is_thin():
62+
oracledb.init_oracle_client(lib_dir=sample_env.get_oracle_client())
6263

6364
# constants
6465
CONNECT_STRING = "localhost/orcl-tg"

src/oracledb/impl/thin/capabilities.pyx

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
#------------------------------------------------------------------------------
2-
# Copyright (c) 2021, 2024, Oracle and/or its affiliates.
2+
# Copyright (c) 2021, 2025, Oracle and/or its affiliates.
33
#
44
# This software is dual-licensed to you under the Universal Permissive License
55
# (UPL) 1.0 as shown at https://oss.oracle.com/licenses/upl and Apache License
@@ -121,7 +121,8 @@ cdef class Capabilities:
121121
self.compile_caps[TNS_CCAP_LOB2] = TNS_CCAP_LOB2_QUASI | \
122122
TNS_CCAP_LOB2_2GB_PREFETCH
123123
self.compile_caps[TNS_CCAP_TTC3] = TNS_CCAP_IMPLICIT_RESULTS | \
124-
TNS_CCAP_BIG_CHUNK_CLR | TNS_CCAP_KEEP_OUT_ORDER
124+
TNS_CCAP_BIG_CHUNK_CLR | TNS_CCAP_KEEP_OUT_ORDER | \
125+
TNS_CCAP_LTXID
125126
self.compile_caps[TNS_CCAP_TTC2] = TNS_CCAP_ZLNP
126127
self.compile_caps[TNS_CCAP_OCI2] = TNS_CCAP_DRCP
127128
self.compile_caps[TNS_CCAP_CLIENT_FN] = TNS_CCAP_CLIENT_FN_MAX

src/oracledb/impl/thin/constants.pxi

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
#------------------------------------------------------------------------------
2-
# Copyright (c) 2020, 2024, Oracle and/or its affiliates.
2+
# Copyright (c) 2020, 2025, Oracle and/or its affiliates.
33
#
44
# This software is dual-licensed to you under the Universal Permissive License
55
# (UPL) 1.0 as shown at https://oss.oracle.com/licenses/upl and Apache License
@@ -384,6 +384,7 @@ cdef enum:
384384
TNS_CCAP_RPC_VERSION_MAX = 7
385385
TNS_CCAP_RPC_SIG_VALUE = 3
386386
TNS_CCAP_DBF_VERSION_MAX = 1
387+
TNS_CCAP_LTXID = 0x08
387388
TNS_CCAP_IMPLICIT_RESULTS = 0x10
388389
TNS_CCAP_BIG_CHUNK_CLR = 0x20
389390
TNS_CCAP_KEEP_OUT_ORDER = 0x80

src/oracledb/impl/thin/messages.pyx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -229,7 +229,7 @@ cdef class Message:
229229
if opcode == TNS_SERVER_PIGGYBACK_LTXID:
230230
buf.read_ub4(&num_bytes)
231231
if num_bytes > 0:
232-
buf.skip_raw_bytes(num_bytes)
232+
self.conn_impl._ltxid = buf.read_bytes()
233233
elif opcode == TNS_SERVER_PIGGYBACK_QUERY_CACHE_INVALIDATION \
234234
or opcode == TNS_SERVER_PIGGYBACK_TRACE_EVENT:
235235
pass

tests/ext/test_ext_2300_tg.py

Lines changed: 152 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,152 @@
1+
# -----------------------------------------------------------------------------
2+
# Copyright (c) 2025, Oracle and/or its affiliates.
3+
#
4+
# This software is dual-licensed to you under the Universal Permissive License
5+
# (UPL) 1.0 as shown at https://oss.oracle.com/licenses/upl and Apache License
6+
# 2.0 as shown at http://www.apache.org/licenses/LICENSE-2.0. You may choose
7+
# either license.
8+
#
9+
# If you elect to accept the software under the Apache License, Version 2.0,
10+
# the following applies:
11+
#
12+
# Licensed under the Apache License, Version 2.0 (the "License");
13+
# you may not use this file except in compliance with the License.
14+
# You may obtain a copy of the License at
15+
#
16+
# https://www.apache.org/licenses/LICENSE-2.0
17+
#
18+
# Unless required by applicable law or agreed to in writing, software
19+
# distributed under the License is distributed on an "AS IS" BASIS,
20+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
21+
# See the License for the specific language governing permissions and
22+
# limitations under the License.
23+
# -----------------------------------------------------------------------------
24+
25+
"""
26+
E2300 - Module for testing Transaction Guard (TG). No special setup is required
27+
but the test suite makes use of debugging packages that are not intended for
28+
normal use. It also creates and drops a service.
29+
"""
30+
31+
import oracledb
32+
import test_env
33+
34+
35+
class TestCase(test_env.BaseTestCase):
36+
service_name = "oracledb-test-tg"
37+
requires_connection = False
38+
39+
@classmethod
40+
def setUpClass(cls):
41+
cls.admin_conn = test_env.get_admin_connection()
42+
user = test_env.get_main_user()
43+
with cls.admin_conn.cursor() as cursor:
44+
cursor.execute(
45+
f"""
46+
declare
47+
params dbms_service.svc_parameter_array;
48+
begin
49+
params('COMMIT_OUTCOME') := 'true';
50+
params('RETENTION_TIMEOUT') := 604800;
51+
dbms_service.create_service('{cls.service_name}',
52+
'{cls.service_name}', params);
53+
dbms_service.start_service('{cls.service_name}');
54+
end;
55+
"""
56+
)
57+
cursor.execute(f"grant execute on dbms_tg_dbg to {user}")
58+
cursor.execute(f"grant execute on dbms_app_cont to {user}")
59+
60+
@classmethod
61+
def tearDownClass(cls):
62+
user = test_env.get_main_user()
63+
with cls.admin_conn.cursor() as cursor:
64+
cursor.execute(f"revoke execute on dbms_tg_dbg from {user}")
65+
cursor.execute(f"revoke execute on dbms_app_cont from {user}")
66+
cursor.callproc("dbms_service.stop_service", [cls.service_name])
67+
cursor.callproc("dbms_service.delete_service", [cls.service_name])
68+
69+
def test_ext_2300(self):
70+
"E2300 - test standalone connection"
71+
params = test_env.get_connect_params().copy()
72+
params.parse_connect_string(test_env.get_connect_string())
73+
params.set(service_name=self.service_name)
74+
for arg_name in ("pre_commit", "post_commit"):
75+
with self.subTest(arg_name=arg_name):
76+
conn = oracledb.connect(params=params)
77+
cursor = conn.cursor()
78+
cursor.execute("truncate table TestTempTable")
79+
cursor.execute(
80+
"""
81+
insert into TestTempTable (IntCol, StringCol1)
82+
values (:1, :2)
83+
""",
84+
[2300, "String for test 2300"],
85+
)
86+
full_arg_name = f"dbms_tg_dbg.tg_failpoint_{arg_name}"
87+
cursor.execute(
88+
f"""
89+
begin
90+
dbms_tg_dbg.set_failpoint({full_arg_name});
91+
end;
92+
"""
93+
)
94+
ltxid = conn.ltxid
95+
with self.assertRaisesFullCode("DPY-4011"):
96+
conn.commit()
97+
conn = oracledb.connect(params=params)
98+
cursor = conn.cursor()
99+
committed_var = cursor.var(bool)
100+
completed_var = cursor.var(bool)
101+
cursor.callproc(
102+
"dbms_app_cont.get_ltxid_outcome",
103+
[ltxid, committed_var, completed_var],
104+
)
105+
expected_value = arg_name == "post_commit"
106+
self.assertEqual(committed_var.getvalue(), expected_value)
107+
self.assertEqual(completed_var.getvalue(), expected_value)
108+
109+
def test_ext_2301(self):
110+
"E2301 - test pooled connection"
111+
params = test_env.get_pool_params().copy()
112+
params.parse_connect_string(test_env.get_connect_string())
113+
params.set(service_name=self.service_name, max=10)
114+
pool = oracledb.create_pool(params=params)
115+
for arg_name in ("pre_commit", "post_commit"):
116+
with self.subTest(arg_name=arg_name):
117+
conn = pool.acquire()
118+
cursor = conn.cursor()
119+
cursor.execute("truncate table TestTempTable")
120+
cursor.execute(
121+
"""
122+
insert into TestTempTable (IntCol, StringCol1)
123+
values (:1, :2)
124+
""",
125+
[2300, "String for test 2300"],
126+
)
127+
full_arg_name = f"dbms_tg_dbg.tg_failpoint_{arg_name}"
128+
cursor.execute(
129+
f"""
130+
begin
131+
dbms_tg_dbg.set_failpoint({full_arg_name});
132+
end;
133+
"""
134+
)
135+
ltxid = conn.ltxid
136+
with self.assertRaisesFullCode("DPY-4011"):
137+
conn.commit()
138+
conn = pool.acquire()
139+
cursor = conn.cursor()
140+
committed_var = cursor.var(bool)
141+
completed_var = cursor.var(bool)
142+
cursor.callproc(
143+
"dbms_app_cont.get_ltxid_outcome",
144+
[ltxid, committed_var, completed_var],
145+
)
146+
expected_value = arg_name == "post_commit"
147+
self.assertEqual(committed_var.getvalue(), expected_value)
148+
self.assertEqual(completed_var.getvalue(), expected_value)
149+
150+
151+
if __name__ == "__main__":
152+
test_env.run_test_cases()

0 commit comments

Comments
 (0)