Skip to content

Commit bce81f2

Browse files
Added support for capturing warnings on connections and cursors.
1 parent dbe6106 commit bce81f2

17 files changed

+242
-38
lines changed

Diff for: doc/src/api_manual/connection.rst

+33-1
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,8 @@
44
API: Connection Objects
55
***********************
66

7-
A Connection object can be created with :meth:`oracledb.connect()`.
7+
A Connection object can be created with :meth:`oracledb.connect()` or with
8+
:meth:`ConnectionPool.acquire()`.
89

910
.. note::
1011

@@ -932,3 +933,34 @@ Connection Attributes
932933
If you connect to Oracle Database 18 or higher using Oracle Client
933934
libraries 12.2 or lower you will only receive the base version (such as
934935
18.0.0.0.0) instead of the full version (such as 18.3.0.0.0).
936+
937+
.. attribute:: Connection.warning
938+
939+
This read-only attribute provides an :ref:`oracledb._Error<exchandling>`
940+
object giving information about any database warnings (such as the password
941+
being in the grace period, or the pool being created with a smaller than
942+
requested size due to database resource restrictions) that were generated
943+
during connection establishment or by :meth:`oracledb.create_pool()`. The
944+
attribute will be present if there was a warning, but creation otherwise
945+
completed successfully. The connection will be usable despite the warning.
946+
947+
For :ref:`standalone connections <standaloneconnection>`,
948+
``Connection.warning`` will be present for the lifetime of the connection.
949+
950+
For :ref:`pooled connections <connpooling>`, ``Connection.warning`` will be
951+
cleared when a connection is released to the pool such as with
952+
:meth:`ConnectionPool.release()`.
953+
954+
In python-oracledb Thick mode, warnings may be generated during pool
955+
creation itself. These warnings will be placed on new connections created
956+
by the pool, provided no warnings were generated by the individual
957+
connection creations, in which case those connection warnings will be
958+
returned.
959+
960+
If no warning was generated the value ``None`` is returned.
961+
962+
.. versionadded:: 2.0.0
963+
964+
.. note::
965+
966+
This attribute is an extension to the DB API definition.

Diff for: doc/src/api_manual/cursor.rst

+18
Original file line numberDiff line numberDiff line change
@@ -634,3 +634,21 @@ Cursor Attributes
634634
.. note::
635635

636636
The DB API definition does not define this attribute.
637+
638+
.. attribute:: Cursor.warning
639+
640+
This read-only attribute provides an :ref:`oracledb._Error<exchandling>`
641+
object giving information about any database warnings (such as PL/SQL
642+
compilation warnings) that were generated during the last call to
643+
:meth:`~Cursor.execute()` or :meth:`~Cursor.executemany()`. This value is
644+
automatically cleared on the next call to :meth:`~Cursor.execute()` or
645+
:meth:`~Cursor.executemany()`. If no warning was generated the value
646+
``None`` is returned.
647+
648+
See :ref:`plsqlwarning` for more information.
649+
650+
.. versionadded:: 2.0.0
651+
652+
.. note::
653+
654+
The DB API definition does not define this attribute.

Diff for: doc/src/release_notes.rst

+5
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,11 @@ Common Changes
4747
++++++++++++++
4848

4949
#) Dropped support for Python 3.6 and added support for Python 3.12.
50+
#) Added property :attr:`Cursor.warning` for database warnings (such as PL/SQL
51+
compilation warnings) generated by calls to :meth:`Cursor.execute()` or
52+
:meth:`Cursor.executemany()`.
53+
#) Added property :attr:`Connection.warning` for warnings (such as the password
54+
being in the grace period) generated during connection.
5055
#) Added properties that provide information about the database:
5156
:attr:`Connection.db_domain`, :attr:`Connection.db_name`,
5257
:attr:`Connection.max_open_cursors`, :attr:`Connection.service_name`

Diff for: doc/src/user_guide/plsql_execution.rst

+43-19
Original file line numberDiff line numberDiff line change
@@ -34,8 +34,8 @@ then the following Python code can be used to call it:
3434
cursor.callproc('myproc', [123, out_val])
3535
print(out_val.getvalue()) # will print 246
3636
37-
Calling :meth:`Cursor.callproc()` actually generates an anonymous PL/SQL block
38-
as shown below, which is then executed:
37+
Calling :meth:`Cursor.callproc()` internally generates an :ref:`anonymous PL/SQL
38+
block <anonplsql>` and executes it. This is equivalent to the application code:
3939

4040
.. code-block:: python
4141
@@ -113,6 +113,8 @@ The Python code that will call this procedure looks as follows:
113113
See :ref:`bind` for information on binding.
114114

115115

116+
.. _anonplsql:
117+
116118
Anonymous PL/SQL Blocks
117119
-----------------------
118120

@@ -134,34 +136,56 @@ Creating Stored Procedures and Packages
134136
---------------------------------------
135137

136138
To create PL/SQL stored procedures and packages, use :meth:`Cursor.execute()`
137-
with a SQL CREATE command.
139+
with a CREATE command. For example:
140+
141+
.. code-block:: python
142+
143+
cursor.execute("""
144+
create or replace procedure myprocedure
145+
(p_in in number, p_out out number) as
146+
begin
147+
p_out := p_in * 2;
148+
end;""")
149+
150+
.. _plsqlwarning:
138151

139-
Creation warning messages can be found from database views like USER_ERRORS.
152+
PL/SQL Compilation Warnings
153+
+++++++++++++++++++++++++++
140154

141-
For example, creating a procedure with an error could be like:
155+
When creating PL/SQL procedures and functions (or creating types) in
156+
python-oracledb, the statement might succeed without throwing an error, but
157+
there may be additional informational messages. (These messages are sometimes
158+
known in Oracle as "success with info" messages). If your application needs to
159+
show such messages, they must be explicitly looked for using
160+
:attr:`Cursor.warning`. A subsequent query from a table like ``USER_ERRORS``
161+
will show more details. For example:
142162

143163
.. code-block:: python
144164
145165
with connection.cursor() as cursor:
166+
146167
cursor.execute("""
147-
create or replace procedure badproc (a in number) as
148-
begin
149-
WRONG WRONG WRONG
150-
end;""")
151-
cursor.execute("""
152-
select line, position, text
153-
from user_errors
154-
where name = 'BADPROC' and type = 'PROCEDURE'
155-
order by name, type, line, position""")
156-
errors = cursor.fetchall()
157-
if errors:
158-
for info in errors:
168+
create or replace procedure badproc as
169+
begin
170+
WRONG WRONG WRONG
171+
end;""")
172+
173+
if cursor.warning.full_code == "DPY-7000":
174+
print(cursor.warning)
175+
176+
# Get details
177+
cursor.execute("""
178+
select line, position, text
179+
from user_errors
180+
where name = 'BADPROC' and type = 'PROCEDURE'
181+
order by name, type, line, position""")
182+
for info in cursor:
159183
print("Error at line {} position {}:\n{}".format(*info))
160-
else:
161-
print("Created successfully")
162184
163185
The output would be::
164186

187+
DPY-7000: creation succeeded with compilation errors
188+
Error at line 3 position 27:
165189
PLS-00103: Encountered the symbol "WRONG" when expecting one of the following:
166190

167191
:= . ( @ % ;

Diff for: src/oracledb/base_impl.pxd

+2
Original file line numberDiff line numberDiff line change
@@ -242,6 +242,7 @@ cdef class BaseConnImpl:
242242
readonly str proxy_user
243243
public object inputtypehandler
244244
public object outputtypehandler
245+
public object warning
245246
public bint autocommit
246247
public bint invoke_session_callback
247248
readonly tuple server_version
@@ -279,6 +280,7 @@ cdef class BaseCursorImpl:
279280
public list bind_vars
280281
public type bind_style
281282
public dict bind_vars_by_name
283+
public object warning
282284
uint32_t _buffer_rowcount
283285
uint32_t _buffer_index
284286
uint32_t _fetch_array_size

Diff for: src/oracledb/connection.py

+10
Original file line numberDiff line numberDiff line change
@@ -1084,6 +1084,16 @@ def version(self) -> str:
10841084
self._version = ".".join(str(c) for c in self._impl.server_version)
10851085
return self._version
10861086

1087+
@property
1088+
def warning(self) -> errors._Error:
1089+
"""
1090+
Returns any warning that was generated when the connection was created,
1091+
or the value None if no warning was generated. The value will be
1092+
cleared for pooled connections after they are returned to the pool.
1093+
"""
1094+
self._verify_connected()
1095+
return self._impl.warning
1096+
10871097
def xid(
10881098
self,
10891099
format_id: int,

Diff for: src/oracledb/cursor.py

+12
Original file line numberDiff line numberDiff line change
@@ -428,6 +428,7 @@ def execute(
428428
self._set_input_sizes = False
429429
if parameters is not None:
430430
impl.bind_one(self, parameters)
431+
impl.warning = None
431432
impl.execute(self)
432433
if impl.fetch_vars is not None:
433434
return self
@@ -882,3 +883,14 @@ def var(
882883
bypass_decode,
883884
convert_nulls=convert_nulls,
884885
)
886+
887+
@property
888+
def warning(self) -> Union[errors._Error, None]:
889+
"""
890+
Returns any warning that was generated during the last call to
891+
execute() or executemany(), or the value None if no warning was
892+
generated. This value will be cleared on the next call to execute() or
893+
executemany().
894+
"""
895+
self._verify_open()
896+
return self._impl.warning

Diff for: src/oracledb/errors.py

+15
Original file line numberDiff line numberDiff line change
@@ -134,6 +134,15 @@ def _get_error_text(error_num: int, **args) -> str:
134134
return f"{ERR_PREFIX}-{error_num:04}: {message}"
135135

136136

137+
def _create_warning(error_num: int, **args) -> _Error:
138+
"""
139+
Returns a warning error object for the specified error number and supplied
140+
arguments.
141+
"""
142+
message = _get_error_text(error_num, **args)
143+
return _Error(message, iswarning=True)
144+
145+
137146
def _raise_err(
138147
error_num: int,
139148
context_error_message: str = None,
@@ -291,6 +300,9 @@ def _raise_from_string(exc_type: Exception, message: str) -> None:
291300
ERR_PROXY_FAILURE = 6004
292301
ERR_CONNECTION_FAILED = 6005
293302

303+
# error numbers that result in Warning
304+
WRN_COMPILATION_ERROR = 7000
305+
294306
# Oracle error number cross reference
295307
ERR_ORACLE_ERROR_XREF = {
296308
28: ERR_CONNECTION_CLOSED,
@@ -309,6 +321,7 @@ def _raise_from_string(exc_type: Exception, message: str) -> None:
309321
24459: ERR_POOL_NO_CONNECTION_AVAILABLE,
310322
24496: ERR_POOL_NO_CONNECTION_AVAILABLE,
311323
24338: ERR_INVALID_REF_CURSOR,
324+
24344: WRN_COMPILATION_ERROR,
312325
38902: ERR_TOO_MANY_BATCH_ERRORS,
313326
}
314327

@@ -331,6 +344,7 @@ def _raise_from_string(exc_type: Exception, message: str) -> None:
331344
4: exceptions.DatabaseError,
332345
5: exceptions.InternalError,
333346
6: exceptions.OperationalError,
347+
7: exceptions.Warning,
334348
}
335349

336350
# error messages that have a troubleshooting section available
@@ -610,4 +624,5 @@ def _raise_from_string(exc_type: Exception, message: str) -> None:
610624
ERR_WRONG_SCROLL_MODE: (
611625
"scroll mode must be relative, absolute, first or last"
612626
),
627+
WRN_COMPILATION_ERROR: "creation succeeded with compilation errors",
613628
}

Diff for: src/oracledb/impl/thick/connection.pyx

+9-3
Original file line numberDiff line numberDiff line change
@@ -271,6 +271,7 @@ cdef class ThickConnImpl(BaseConnImpl):
271271
ConnectParamsImpl pool_params
272272
dpiAccessToken access_token
273273
dpiVersionInfo version_info
274+
dpiErrorInfo error_info
274275
ConnectionParams params
275276
int status
276277

@@ -388,10 +389,15 @@ cdef class ThickConnImpl(BaseConnImpl):
388389
params.password_len, params.dsn_ptr,
389390
params.dsn_len, &common_params,
390391
&conn_params, &self._handle)
391-
if status == DPI_SUCCESS:
392-
status = dpiConn_getServerVersion(self._handle, NULL, NULL,
393-
&version_info)
392+
dpiContext_getError(driver_context, &error_info)
394393
if status < 0:
394+
_raise_from_info(&error_info)
395+
elif error_info.isWarning:
396+
self.warning = _create_new_from_info(&error_info)
397+
if conn_params.outNewSession and self.warning is None:
398+
self.warning = pool_impl.warning
399+
if dpiConn_getServerVersion(self._handle, NULL, NULL,
400+
&version_info) < 0:
395401
_raise_from_odpi()
396402
self.server_version = (
397403
version_info.versionNum,

Diff for: src/oracledb/impl/thick/cursor.pyx

+9-3
Original file line numberDiff line numberDiff line change
@@ -244,6 +244,7 @@ cdef class ThickCursorImpl(BaseCursorImpl):
244244
"""
245245
cdef:
246246
uint32_t mode, num_query_cols
247+
dpiErrorInfo error_info
247248
uint64_t rowcount = 0
248249
int status
249250
if self.bind_vars is not None:
@@ -254,10 +255,14 @@ cdef class ThickCursorImpl(BaseCursorImpl):
254255
mode = DPI_MODE_EXEC_DEFAULT
255256
with nogil:
256257
status = dpiStmt_execute(self._handle, mode, &num_query_cols)
257-
if status == DPI_SUCCESS and not self._stmt_info.isPLSQL:
258+
if status == DPI_SUCCESS:
259+
dpiContext_getError(driver_context, &error_info)
260+
if not self._stmt_info.isPLSQL:
258261
status = dpiStmt_getRowCount(self._handle, &rowcount)
259262
if status < 0:
260263
_raise_from_odpi()
264+
elif error_info.isWarning:
265+
self.warning = _create_new_from_info(&error_info)
261266
self.rowcount = rowcount
262267
if num_query_cols > 0:
263268
self._perform_define(cursor, num_query_cols)
@@ -289,13 +294,14 @@ cdef class ThickCursorImpl(BaseCursorImpl):
289294
if num_execs_int > 0:
290295
with nogil:
291296
status = dpiStmt_executeMany(self._handle, mode, num_execs_int)
292-
if status < 0:
293-
dpiContext_getError(driver_context, &error_info)
297+
dpiContext_getError(driver_context, &error_info)
294298
if not self._stmt_info.isPLSQL:
295299
dpiStmt_getRowCount(self._handle, &rowcount)
296300
self.rowcount = rowcount
297301
if status < 0:
298302
_raise_from_info(&error_info)
303+
elif error_info.isWarning:
304+
self.warning = _create_new_from_info(&error_info)
299305
if self._stmt_info.isReturning or self._stmt_info.isPLSQL:
300306
self._transform_binds()
301307

Diff for: src/oracledb/impl/thick/pool.pyx

+6-1
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ cdef int _token_callback_handler(void *context,
3939
cdef class ThickPoolImpl(BasePoolImpl):
4040
cdef:
4141
dpiPool *_handle
42+
object warning
4243

4344
def __init__(self, str dsn, PoolParamsImpl params):
4445
cdef:
@@ -55,6 +56,7 @@ cdef class ThickPoolImpl(BasePoolImpl):
5556
const char *token_ptr = NULL
5657
const char *private_key_ptr = NULL
5758
dpiAccessToken access_token
59+
dpiErrorInfo error_info
5860
str token, private_key
5961
int status
6062

@@ -144,8 +146,11 @@ cdef class ThickPoolImpl(BasePoolImpl):
144146
password_ptr, password_len, dsn_ptr,
145147
dsn_len, &common_params, &create_params,
146148
&self._handle)
149+
dpiContext_getError(driver_context, &error_info)
147150
if status < 0:
148-
_raise_from_odpi()
151+
_raise_from_info(&error_info)
152+
elif error_info.isWarning:
153+
self.warning = _create_new_from_info(&error_info)
149154

150155
name_bytes = create_params.outPoolName[:create_params.outPoolNameLength]
151156
self.name = name_bytes.decode()

Diff for: src/oracledb/impl/thin/cursor.pyx

+2
Original file line numberDiff line numberDiff line change
@@ -130,6 +130,7 @@ cdef class ThinCursorImpl(BaseCursorImpl):
130130
message = self._create_message(ExecuteMessage, cursor)
131131
message.num_execs = 1
132132
self._conn_impl._protocol._process_single_message(message)
133+
self.warning = message.warning
133134
if self._statement._is_query:
134135
self.rowcount = 0
135136
if message.type_cache is not None:
@@ -166,6 +167,7 @@ cdef class ThinCursorImpl(BaseCursorImpl):
166167
message.offset = 1
167168
message.num_execs = num_execs - 1
168169
self._conn_impl._protocol._process_single_message(message)
170+
self.warning = message.warning
169171

170172
def get_array_dml_row_counts(self):
171173
if self._dmlrowcounts is None:

0 commit comments

Comments
 (0)