Compare commits

...

16 Commits
main ... v1.3.x

Author SHA1 Message Date
Anthony Tuininga 99bd32317f Preparing to release 1.3.2. 2023-06-15 16:00:40 -06:00
Anthony Tuininga 408f3684bd Avoid spurious errors due to the environment variable TNS_ADMIN being
unset or set to an empty or invalid value.
2023-06-15 15:31:51 -06:00
Anthony Tuininga 76ad274e05 Tweak product name for correctness. 2023-06-15 15:31:31 -06:00
Anthony Tuininga eeda0162d1 Fixed bug when using DRCP with a 23c Oracle Database. 2023-06-15 15:30:37 -06:00
Anthony Tuininga d480381035 Tweak release notes. 2023-06-15 15:30:22 -06:00
Anthony Tuininga cafbf3761d When fetching REF cursors, only the cursor's arraysize attribute is
considered. Previously, the cursor's prefetchrows attribute was also
considered, but in differing ways between thin and thick modes.
2023-06-15 15:28:59 -06:00
Anthony Tuininga 132e6c5222 Fixed bug when executing PL/SQL with a large number of binds. 2023-06-15 15:22:38 -06:00
Anthony Tuininga 10be0c949f Fixed bug which could cause a redirect loop with improperly configured
listener redirects.
2023-06-15 15:21:11 -06:00
Anthony Tuininga 83d2e98f49 Eliminated unneeded round trip when using token authentication to
connect to the database.
2023-06-15 15:20:43 -06:00
Anthony Tuininga aa42ae84ba Fixed bug which caused a cursor leak if an error was thrown while
processing the execution of a query.
2023-06-15 15:14:42 -06:00
Anthony Tuininga e0f06dbd47 Fixed bugs in the implementation of the statement cache. 2023-06-15 15:06:42 -06:00
Anthony Tuininga 5ec3f5cc30 Fixed bug connecting to databases with older 11g password verifiers
(#189).
2023-06-15 15:02:55 -06:00
Anthony Tuininga 9f7a1c3490 If the externalauth flag is set, don't attempt to parse out a user name
and password since there won't be any!
2023-06-15 15:02:28 -06:00
Anthony Tuininga 636be6278c Fixed bug when using external authentication with a TNS alias (#178). 2023-06-15 14:55:41 -06:00
Anthony Tuininga 09b1b2f719 Improve diagnosibility of unhandled message types. 2023-06-15 14:54:36 -06:00
Anthony Tuininga ce8655f572 Bump version. 2023-06-15 14:52:14 -06:00
18 changed files with 222 additions and 93 deletions

View File

@ -7,6 +7,44 @@ python-oracledb Release Notes
For deprecations, see :ref:`Deprecations <deprecations>`. For deprecations, see :ref:`Deprecations <deprecations>`.
oracledb 1.3.2 (June 2023)
--------------------------
Thin Mode Changes
+++++++++++++++++
#) Fixed bug using :attr:`Cursor.arraysize` for tuning data fetches from REF
CURSORS.
#) Fixed bug connecting to databases with older 11g password verifiers
(`issue 189 <https://github.com/oracle/python-oracledb/issues/189>`__).
#) Fixed bugs in the implementation of the statement cache.
#) Fixed bug which caused a cursor leak if an error was thrown while
processing the execution of a query.
#) Eliminated unneeded round trip when using token authentication to connect
to the database.
#) Fixed bug which could cause a redirect loop with improperly configured
listener redirects.
#) Fixed bug when executing PL/SQL with a large number of binds.
#) Fixed bug when using DRCP with Oracle Database 23c.
Thick Mode Changes
++++++++++++++++++
#) Fixed bug when using external authentication with a Net Service Name
connection string
(`issue 178 <https://github.com/oracle/python-oracledb/issues/178>`__).
#) Fixed bug when using external authentication with an Easy Connect
connection string.
Common Changes
++++++++++++++
#) When fetching rows from REF CURSORS, the cursor's
:attr:`~Cursor.prefetchrows` attribute is now ignored. Use
:attr:`Cursor.arraysize` for tuning these fetches. This change allows
consistency between Thin and Thick modes.
oracledb 1.3.1 (April 2023) oracledb 1.3.1 (April 2023)
--------------------------- ---------------------------

View File

@ -245,11 +245,10 @@ Avoiding Premature Prefetching
There are two cases that will benefit from setting ``prefetchrows`` to zero: There are two cases that will benefit from setting ``prefetchrows`` to zero:
* When passing REF CURSORS into PL/SQL packages. Setting ``prefetchrows`` to 0 * When passing REF CURSORS *into* PL/SQL packages. Setting ``prefetchrows`` to
can stop rows being prematurely (and silently) fetched into the 0 can stop rows being prematurely (and silently) fetched into the
python-oracledb or Oracle Client (in python-oracledb Thick mode) internal python-oracledb internal buffer, making those rows unavailable to the PL/SQL
buffer, making those rows unavailable to the PL/SQL code that receives the code that receives the REF CURSOR.
REF CURSOR.
* When querying a PL/SQL function that uses PIPE ROW to emit rows at * When querying a PL/SQL function that uses PIPE ROW to emit rows at
intermittent intervals. By default, several rows needs to be emitted by the intermittent intervals. By default, several rows needs to be emitted by the
@ -259,28 +258,34 @@ There are two cases that will benefit from setting ``prefetchrows`` to zero:
Tuning Fetching from REF CURSORS Tuning Fetching from REF CURSORS
-------------------------------- --------------------------------
In python-oracledb, fetching data from REF CURSORS can be tuned by setting the The internal buffering and performance of fetching data from REF CURSORS can be
values of ``arraysize`` and ``prefetchrows``. The ``prefetchrows`` value must tuned by setting the value of ``arraysize`` before rows are fetched from the
be set before calling the PL/SQL procedure because the REF CURSOR is executed cursor. The ``prefetchrows`` value is ignored when fetching *from* REF CURSORS.
on the server.
For example: For example:
.. code-block:: python .. code-block:: python
# Set the arraysize and prefetch rows of the REF cursor
ref_cursor = connection.cursor() ref_cursor = connection.cursor()
ref_cursor.prefetchrows = 1000
ref_cursor.arraysize = 1000
# Perform the tuned fetch
sum_rows = 0
cursor.callproc("myrefcursorproc", [ref_cursor]) cursor.callproc("myrefcursorproc", [ref_cursor])
ref_cursor.arraysize = 1000
print("Sum of IntCol for", num_rows, "rows:") print("Sum of IntCol for", num_rows, "rows:")
for row in ref_cursor: for row in ref_cursor:
sum_rows += row[0] sum_rows += row[0]
print(sum_rows) print(sum_rows)
The ``arraysize`` value can also be set before calling the procedure:
.. code-block:: python
ref_cursor = connection.cursor()
ref_cursor.arraysize = 1000
cursor.callproc("myrefcursorproc", [ref_cursor])
for row in ref_cursor:
. . .
.. _roundtrips: .. _roundtrips:
Also see `Avoiding Premature Prefetching`_. Also see `Avoiding Premature Prefetching`_.

View File

@ -412,7 +412,8 @@ ERR_MESSAGE_FORMATS = {
ERR_MESSAGE_HAS_NO_PAYLOAD: ERR_MESSAGE_HAS_NO_PAYLOAD:
'message has no payload', 'message has no payload',
ERR_MESSAGE_TYPE_UNKNOWN: ERR_MESSAGE_TYPE_UNKNOWN:
'internal error: unknown protocol message type {message_type}', 'internal error: unknown protocol message type {message_type} '
'at position {position}',
ERR_MISSING_BIND_VALUE: ERR_MISSING_BIND_VALUE:
'a bind variable replacement value for placeholder ":{name}" was ' 'a bind variable replacement value for placeholder ":{name}" was '
'not provided', 'not provided',

View File

@ -572,6 +572,9 @@ cdef class ConnectParamsImpl:
if pos >= 0: if pos >= 0:
user = credentials[:pos] or None user = credentials[:pos] or None
password = credentials[pos + 1:] or None password = credentials[pos + 1:] or None
elif connect_string is None:
connect_string = dsn or None
user = password = None
else: else:
user = credentials or None user = credentials or None
password = None password = None
@ -605,7 +608,7 @@ cdef class ConnectParamsImpl:
""" """
if kwargs: if kwargs:
self.set(kwargs) self.set(kwargs)
if self.user is None and dsn is not None: if self.user is None and not self.externalauth and dsn is not None:
user, password, dsn = self.parse_dsn_with_credentials(dsn) user, password, dsn = self.parse_dsn_with_credentials(dsn)
self.set(dict(user=user, password=password)) self.set(dict(user=user, password=password))
if dsn is not None and thin: if dsn is not None and thin:

View File

@ -199,9 +199,6 @@ cdef class ThickVarImpl(BaseVarImpl):
if dpiStmt_addRef(data.value.asStmt) < 0: if dpiStmt_addRef(data.value.asStmt) < 0:
_raise_from_odpi() _raise_from_odpi()
cursor_impl._handle = data.value.asStmt cursor_impl._handle = data.value.asStmt
if dpiStmt_setPrefetchRows(cursor_impl._handle,
cursor_impl.prefetchrows) < 0:
_raise_from_odpi()
cursor_impl._fixup_ref_cursor = True cursor_impl._fixup_ref_cursor = True
cursor.statement = None cursor.statement = None

View File

@ -237,11 +237,12 @@ cdef class ThinConnImpl(BaseConnImpl):
if statement is None: if statement is None:
statement = Statement() statement = Statement()
statement._prepare(sql) statement._prepare(sql)
if len(self._statement_cache) < self._statement_cache_size \ if cache_statement and not self._drcp_establish_session \
and cache_statement \ and not statement._is_ddl \
and not self._drcp_establish_session: and self._statement_cache_size > 0:
self._statement_cache[sql] = statement
statement._return_to_cache = True statement._return_to_cache = True
self._statement_cache[sql] = statement
self._adjust_statement_cache()
elif statement._in_use or not cache_statement \ elif statement._in_use or not cache_statement \
or self._drcp_establish_session: or self._drcp_establish_session:
if not cache_statement: if not cache_statement:
@ -283,8 +284,6 @@ cdef class ThinConnImpl(BaseConnImpl):
with self._statement_cache_lock: with self._statement_cache_lock:
if statement._return_to_cache: if statement._return_to_cache:
statement._in_use = False statement._in_use = False
self._statement_cache.move_to_end(statement._sql)
self._adjust_statement_cache()
elif statement._cursor_id != 0: elif statement._cursor_id != 0:
self._add_cursor_to_close(statement) self._add_cursor_to_close(statement)

View File

@ -40,6 +40,7 @@ DEF TNS_PACKET_TYPE_CONTROL = 14
DEF TNS_PACKET_TYPE_REDIRECT = 5 DEF TNS_PACKET_TYPE_REDIRECT = 5
# packet flags # packet flags
DEF TNS_PACKET_FLAG_REDIRECT = 0x04
DEF TNS_PACKET_FLAG_TLS_RENEG = 0x08 DEF TNS_PACKET_FLAG_TLS_RENEG = 0x08
# data flags # data flags

View File

@ -104,10 +104,10 @@ cdef class ThinCursorImpl(BaseCursorImpl):
cdef MessageWithData message cdef MessageWithData message
if self._statement._requires_full_execute: if self._statement._requires_full_execute:
message = self._create_message(ExecuteMessage, cursor) message = self._create_message(ExecuteMessage, cursor)
message.num_execs = self._fetch_array_size
else: else:
message = self._create_message(FetchMessage, cursor) message = self._create_message(FetchMessage, cursor)
self._conn_impl._protocol._process_single_message(message) self._conn_impl._protocol._process_single_message(message)
self._statement._requires_full_execute = False
cdef BaseConnImpl _get_conn_impl(self): cdef BaseConnImpl _get_conn_impl(self):
""" """

View File

@ -177,7 +177,8 @@ cdef class Message:
self._process_server_side_piggyback(buf) self._process_server_side_piggyback(buf)
else: else:
errors._raise_err(errors.ERR_MESSAGE_TYPE_UNKNOWN, errors._raise_err(errors.ERR_MESSAGE_TYPE_UNKNOWN,
message_type=message_type) message_type=message_type,
position=buf._pos - 1)
cdef int _process_return_parameters(self, ReadBuffer buf) except -1: cdef int _process_return_parameters(self, ReadBuffer buf) except -1:
raise NotImplementedError() raise NotImplementedError()
@ -351,7 +352,6 @@ cdef class MessageWithData(Message):
cursor = self.cursor.connection.cursor() cursor = self.cursor.connection.cursor()
cursor_impl = cursor._impl cursor_impl = cursor._impl
cursor_impl._statement = Statement() cursor_impl._statement = Statement()
cursor_impl._fetch_array_size = cursor.arraysize + cursor.prefetchrows
cursor_impl._more_rows_to_fetch = True cursor_impl._more_rows_to_fetch = True
cursor_impl._statement._is_query = True cursor_impl._statement._is_query = True
cursor_impl._statement._requires_full_execute = True cursor_impl._statement._requires_full_execute = True
@ -740,7 +740,8 @@ cdef class MessageWithData(Message):
ThinConnImpl conn_impl = self.conn_impl ThinConnImpl conn_impl = self.conn_impl
object exc_type object exc_type
Message._process_error_info(self, buf) Message._process_error_info(self, buf)
cursor_impl._statement._cursor_id = self.error_info.cursor_id if self.error_info.cursor_id != 0:
cursor_impl._statement._cursor_id = self.error_info.cursor_id
if not cursor_impl._statement._is_plsql and not self.in_fetch: if not cursor_impl._statement._is_plsql and not self.in_fetch:
cursor_impl.rowcount = self.error_info.rowcount cursor_impl.rowcount = self.error_info.rowcount
cursor_impl._lastrowid = self.error_info.rowid cursor_impl._lastrowid = self.error_info.rowid
@ -785,13 +786,15 @@ cdef class MessageWithData(Message):
It indicates whether binds are IN only, IN/OUT or OUT only. It indicates whether binds are IN only, IN/OUT or OUT only.
""" """
cdef: cdef:
uint16_t i, num_binds, num_bytes, temp16 uint16_t i, num_bytes, temp16
uint32_t temp32, num_binds
BindInfo bind_info BindInfo bind_info
buf.skip_ub1() # flag buf.skip_ub1() # flag
buf.read_ub2(&num_binds) # num requests buf.read_ub2(&temp16) # num requests
buf.read_ub4(&self.row_index) # iter num buf.read_ub4(&temp32) # num iters
num_binds = temp32 * 256 + temp16
buf.skip_ub4() # num iters this time buf.skip_ub4() # num iters this time
buf.read_ub2(&temp16) # uac buffer length buf.skip_ub2() # uac buffer length
buf.read_ub2(&num_bytes) # bit vector for fast fetch buf.read_ub2(&num_bytes) # bit vector for fast fetch
if num_bytes > 0: if num_bytes > 0:
buf.skip_raw_bytes(num_bytes) buf.skip_raw_bytes(num_bytes)
@ -1325,20 +1328,18 @@ cdef class AuthMessage(Message):
newpassword_with_salt) newpassword_with_salt)
self.encoded_newpassword = encrypted_newpassword.hex().upper() self.encoded_newpassword = encrypted_newpassword.hex().upper()
cdef int _generate_verifier(self, bint verifier_11g) except -1: cdef int _generate_verifier(self) except -1:
""" """
Generate the multi-round verifier. Generate the multi-round verifier.
""" """
cdef bytes jdwp_data cdef:
bytes jdwp_data
bytearray b
ssize_t i
# create password hash # create password hash
verifier_data = bytes.fromhex(self.session_data['AUTH_VFR_DATA']) verifier_data = bytes.fromhex(self.session_data['AUTH_VFR_DATA'])
if verifier_11g: if self.verifier_type == TNS_VERIFIER_TYPE_12C:
keylen = 24
h = hashlib.sha1(self.password)
h.update(verifier_data)
password_hash = h.digest() + bytes(4)
else:
keylen = 32 keylen = 32
iterations = int(self.session_data['AUTH_PBKDF2_VGEN_COUNT']) iterations = int(self.session_data['AUTH_PBKDF2_VGEN_COUNT'])
salt = verifier_data + b'AUTH_PBKDF2_SPEEDY_KEY' salt = verifier_data + b'AUTH_PBKDF2_SPEEDY_KEY'
@ -1348,28 +1349,42 @@ cdef class AuthMessage(Message):
h.update(password_key) h.update(password_key)
h.update(verifier_data) h.update(verifier_data)
password_hash = h.digest()[:32] password_hash = h.digest()[:32]
else:
keylen = 24
h = hashlib.sha1(self.password)
h.update(verifier_data)
password_hash = h.digest() + bytes(4)
# decrypt first half of session key # decrypt first half of session key
encoded_server_key = bytes.fromhex(self.session_data['AUTH_SESSKEY']) encoded_server_key = bytes.fromhex(self.session_data['AUTH_SESSKEY'])
session_key_part_a = decrypt_cbc(password_hash, encoded_server_key) session_key_part_a = decrypt_cbc(password_hash, encoded_server_key)
# generate second half of session key # generate second half of session key
session_key_part_b = secrets.token_bytes(32) session_key_part_b = secrets.token_bytes(len(session_key_part_a))
encoded_client_key = encrypt_cbc(password_hash, session_key_part_b) encoded_client_key = encrypt_cbc(password_hash, session_key_part_b)
self.session_key = encoded_client_key.hex().upper()[:64]
# create session key from combo key # create session key and combo key
mixing_salt = bytes.fromhex(self.session_data['AUTH_PBKDF2_CSK_SALT']) if len(session_key_part_a) == 48:
iterations = int(self.session_data['AUTH_PBKDF2_SDER_COUNT']) self.session_key = encoded_client_key.hex().upper()[:96]
temp_key = session_key_part_b[:keylen] + session_key_part_a[:keylen] b = bytearray(24)
combo_key = get_derived_key(temp_key.hex().upper().encode(), for i in range(16, 40):
mixing_salt, keylen, iterations) b[i - 16] = session_key_part_a[i] ^ session_key_part_b[i]
part1 = hashlib.md5(b[:16]).digest()
part2 = hashlib.md5(b[16:]).digest()
combo_key = (part1 + part2)[:keylen]
else:
self.session_key = encoded_client_key.hex().upper()[:64]
salt = bytes.fromhex(self.session_data['AUTH_PBKDF2_CSK_SALT'])
iterations = int(self.session_data['AUTH_PBKDF2_SDER_COUNT'])
temp_key = session_key_part_b[:keylen] + session_key_part_a[:keylen]
combo_key = get_derived_key(temp_key.hex().upper().encode(), salt,
keylen, iterations)
# retain session key for use by the change password API # retain session key for use by the change password API
self.conn_impl._combo_key = combo_key self.conn_impl._combo_key = combo_key
# generate speedy key for 12c verifiers # generate speedy key for 12c verifiers
if not verifier_11g: if self.verifier_type == TNS_VERIFIER_TYPE_12C:
salt = secrets.token_bytes(16) salt = secrets.token_bytes(16)
speedy_key = encrypt_cbc(combo_key, salt + password_key) speedy_key = encrypt_cbc(combo_key, salt + password_key)
self.speedy_key = speedy_key[:80].hex().upper() self.speedy_key = speedy_key[:80].hex().upper()
@ -1537,7 +1552,6 @@ cdef class AuthMessage(Message):
cdef int _write_message(self, WriteBuffer buf) except -1: cdef int _write_message(self, WriteBuffer buf) except -1:
cdef: cdef:
uint8_t has_user = 1 if self.user_bytes_len > 0 else 0 uint8_t has_user = 1 if self.user_bytes_len > 0 else 0
bint verifier_11g = False
uint32_t num_pairs uint32_t num_pairs
# perform final determination of data to write # perform final determination of data to write
@ -1557,15 +1571,13 @@ cdef class AuthMessage(Message):
else: else:
num_pairs += 2 num_pairs += 2
self.auth_mode |= TNS_AUTH_MODE_WITH_PASSWORD self.auth_mode |= TNS_AUTH_MODE_WITH_PASSWORD
if self.verifier_type in (TNS_VERIFIER_TYPE_11G_1, if self.verifier_type == TNS_VERIFIER_TYPE_12C:
TNS_VERIFIER_TYPE_11G_2): num_pairs += 1
verifier_11g = True elif self.verifier_type not in (TNS_VERIFIER_TYPE_11G_1,
elif self.verifier_type != TNS_VERIFIER_TYPE_12C: TNS_VERIFIER_TYPE_11G_2):
errors._raise_err(errors.ERR_UNSUPPORTED_VERIFIER_TYPE, errors._raise_err(errors.ERR_UNSUPPORTED_VERIFIER_TYPE,
verifier_type=self.verifier_type) verifier_type=self.verifier_type)
else: self._generate_verifier()
num_pairs += 1
self._generate_verifier(verifier_11g)
# determine which other key/value pairs to write # determine which other key/value pairs to write
if self.newpassword is not None: if self.newpassword is not None:
@ -1613,7 +1625,7 @@ cdef class AuthMessage(Message):
self._write_key_value(buf, "AUTH_TOKEN", self.token) self._write_key_value(buf, "AUTH_TOKEN", self.token)
elif not self.change_password: elif not self.change_password:
self._write_key_value(buf, "AUTH_SESSKEY", self.session_key, 1) self._write_key_value(buf, "AUTH_SESSKEY", self.session_key, 1)
if not verifier_11g: if self.verifier_type == TNS_VERIFIER_TYPE_12C:
self._write_key_value(buf, "AUTH_PBKDF2_SPEEDY_KEY", self._write_key_value(buf, "AUTH_PBKDF2_SPEEDY_KEY",
self.speedy_key) self.speedy_key)
if self.encoded_password is not None: if self.encoded_password is not None:
@ -1735,7 +1747,7 @@ cdef class ConnectMessage(Message):
if buf._caps.supports_oob: if buf._caps.supports_oob:
service_options |= TNS_GSO_CAN_RECV_ATTENTION service_options |= TNS_GSO_CAN_RECV_ATTENTION
connect_flags_2 |= TNS_CHECK_OOB connect_flags_2 |= TNS_CHECK_OOB
buf.start_request(TNS_PACKET_TYPE_CONNECT) buf.start_request(TNS_PACKET_TYPE_CONNECT, self.packet_flags)
buf.write_uint16(TNS_VERSION_DESIRED) buf.write_uint16(TNS_VERSION_DESIRED)
buf.write_uint16(TNS_VERSION_MINIMUM) buf.write_uint16(TNS_VERSION_MINIMUM)
buf.write_uint16(service_options) buf.write_uint16(service_options)
@ -1843,13 +1855,13 @@ cdef class ExecuteMessage(MessageWithData):
if self.parse_only: if self.parse_only:
options |= TNS_EXEC_OPTION_DESCRIBE options |= TNS_EXEC_OPTION_DESCRIBE
else: else:
if self.cursor_impl.prefetchrows > 0:
options |= TNS_EXEC_OPTION_FETCH
if stmt._cursor_id == 0 or stmt._requires_define: if stmt._cursor_id == 0 or stmt._requires_define:
num_iters = self.cursor_impl.prefetchrows num_iters = self.cursor_impl.prefetchrows
self.cursor_impl._fetch_array_size = num_iters
else: else:
num_iters = self.cursor_impl._fetch_array_size num_iters = self.cursor_impl.arraysize
self.cursor_impl._fetch_array_size = num_iters
if num_iters > 0:
options |= TNS_EXEC_OPTION_FETCH
if not stmt._is_plsql and not self.parse_only: if not stmt._is_plsql and not self.parse_only:
options |= TNS_EXEC_OPTION_NOT_PLSQL options |= TNS_EXEC_OPTION_NOT_PLSQL
elif stmt._is_plsql and num_params > 0: elif stmt._is_plsql and num_params > 0:
@ -2226,3 +2238,26 @@ cdef class RollbackMessage(Message):
Perform initialization. Perform initialization.
""" """
self.function_code = TNS_FUNC_ROLLBACK self.function_code = TNS_FUNC_ROLLBACK
@cython.final
cdef class SessionReleaseMessage(Message):
cdef:
uint32_t release_mode
cdef int _initialize_hook(self) except -1:
"""
Perform initialization.
"""
self.message_type = TNS_MSG_TYPE_ONEWAY_FN
self.function_code = TNS_FUNC_SESSION_RELEASE
cdef int _write_message(self, WriteBuffer buf) except -1:
"""
Write the message for a DRCP session release.
"""
self._write_function_code(buf)
buf.write_uint8(0) # pointer (tag name)
buf.write_uint8(0) # tag name length
buf.write_ub4(self.release_mode) # mode

View File

@ -587,6 +587,7 @@ cdef class WriteBuffer(Buffer):
cdef: cdef:
uint8_t _packet_type uint8_t _packet_type
uint8_t _packet_flags
Capabilities _caps Capabilities _caps
object _socket object _socket
uint8_t _seq_num uint8_t _seq_num
@ -610,7 +611,7 @@ cdef class WriteBuffer(Buffer):
self.write_uint16(size) self.write_uint16(size)
self.write_uint16(0) self.write_uint16(0)
self.write_uint8(self._packet_type) self.write_uint8(self._packet_type)
self.write_uint8(0) self.write_uint8(self._packet_flags)
self.write_uint16(0) self.write_uint16(0)
self._pos = size self._pos = size
if DEBUG_PACKETS: if DEBUG_PACKETS:
@ -650,7 +651,8 @@ cdef class WriteBuffer(Buffer):
""" """
return self._max_size - PACKET_HEADER_SIZE - 2 return self._max_size - PACKET_HEADER_SIZE - 2
cdef void start_request(self, uint8_t packet_type, uint16_t data_flags=0): cdef void start_request(self, uint8_t packet_type, uint8_t packet_flags=0,
uint16_t data_flags=0):
""" """
Indicates that a request from the client is starting. The packet type Indicates that a request from the client is starting. The packet type
is retained just in case a request spans multiple packets. The packet is retained just in case a request spans multiple packets. The packet
@ -659,6 +661,7 @@ cdef class WriteBuffer(Buffer):
""" """
self._packet_sent = False self._packet_sent = False
self._packet_type = packet_type self._packet_type = packet_type
self._packet_flags = packet_flags
self._pos = PACKET_HEADER_SIZE self._pos = PACKET_HEADER_SIZE
if packet_type == TNS_PACKET_TYPE_DATA: if packet_type == TNS_PACKET_TYPE_DATA:
self.write_uint16(data_flags) self.write_uint16(data_flags)

View File

@ -102,7 +102,7 @@ cdef class Protocol:
message = conn_impl._create_message(RollbackMessage) message = conn_impl._create_message(RollbackMessage)
self._process_message(message) self._process_message(message)
if conn_impl._drcp_enabled: if conn_impl._drcp_enabled:
self._release_drcp_session(self._write_buf, release_mode) self._release_drcp_session(conn_impl, release_mode)
conn_impl._drcp_establish_session = True conn_impl._drcp_establish_session = True
# if the connection is part of a pool, return it to the pool # if the connection is part of a pool, return it to the pool
@ -133,11 +133,11 @@ cdef class Protocol:
""" """
cdef: cdef:
ConnectMessage connect_message = None ConnectMessage connect_message = None
uint8_t packet_type, packet_flags = 0
object ssl_context, connect_info object ssl_context, connect_info
ConnectParamsImpl temp_params ConnectParamsImpl temp_params
str host, redirect_data str host, redirect_data
Address temp_address Address temp_address
uint8_t packet_type
int port, pos int port, pos
# store whether OOB processing is possible or not # store whether OOB processing is possible or not
@ -163,6 +163,7 @@ cdef class Protocol:
connect_message.connect_string_bytes = connect_string.encode() connect_message.connect_string_bytes = connect_string.encode()
connect_message.connect_string_len = \ connect_message.connect_string_len = \
<uint16_t> len(connect_message.connect_string_bytes) <uint16_t> len(connect_message.connect_string_bytes)
connect_message.packet_flags = packet_flags
# process connection message # process connection message
self._process_message(connect_message) self._process_message(connect_message)
@ -180,6 +181,7 @@ cdef class Protocol:
connect_string = redirect_data[pos + 1:] connect_string = redirect_data[pos + 1:]
self._connect_tcp(params, description, address, host, port) self._connect_tcp(params, description, address, host, port)
connect_message = None connect_message = None
packet_flags = TNS_PACKET_FLAG_REDIRECT
elif connect_message.packet_type == TNS_PACKET_TYPE_ACCEPT: elif connect_message.packet_type == TNS_PACKET_TYPE_ACCEPT:
break break
@ -226,7 +228,8 @@ cdef class Protocol:
auth_message = conn_impl._create_message(AuthMessage) auth_message = conn_impl._create_message(AuthMessage)
auth_message._set_params(params, description) auth_message._set_params(params, description)
self._process_message(auth_message) self._process_message(auth_message)
self._process_message(auth_message) if auth_message.resend:
self._process_message(auth_message)
# mark protocol to indicate that connect is no longer in progress; this # mark protocol to indicate that connect is no longer in progress; this
# allows the normal break/reset mechanism to fire # allows the normal break/reset mechanism to fire
@ -292,7 +295,7 @@ cdef class Protocol:
""" """
Send the final close packet to the server and close the socket. Send the final close packet to the server and close the socket.
""" """
buf.start_request(TNS_PACKET_TYPE_DATA, TNS_DATA_FLAGS_EOF) buf.start_request(TNS_PACKET_TYPE_DATA, 0, TNS_DATA_FLAGS_EOF)
buf.end_request() buf.end_request()
self._socket.shutdown(socket.SHUT_RDWR) self._socket.shutdown(socket.SHUT_RDWR)
self._socket.close() self._socket.close()
@ -394,20 +397,16 @@ cdef class Protocol:
buf.skip_raw_bytes(3) buf.skip_raw_bytes(3)
message.error_info.message = buf.read_str(TNS_CS_IMPLICIT) message.error_info.message = buf.read_str(TNS_CS_IMPLICIT)
cdef int _release_drcp_session(self, WriteBuffer buf, cdef int _release_drcp_session(self, ThinConnImpl conn_impl,
uint32_t release_mode) except -1: uint32_t release_mode) except -1:
""" """
Release the session back to DRCP. Standalone sessions are marked for Release the session back to DRCP. Standalone sessions are marked for
deauthentication. deauthentication.
""" """
buf.start_request(TNS_PACKET_TYPE_DATA) cdef SessionReleaseMessage message
buf.write_uint8(TNS_MSG_TYPE_ONEWAY_FN) message = conn_impl._create_message(SessionReleaseMessage)
buf.write_uint8(TNS_FUNC_SESSION_RELEASE) message.release_mode = release_mode
buf.write_uint8(0) # seq number message.send(self._write_buf)
buf.write_uint8(0) # pointer (tag name)
buf.write_uint8(0) # tag name length
buf.write_ub4(release_mode) # mode
buf.end_request()
cdef int _reset(self, Message message) except -1: cdef int _reset(self, Message message) except -1:
cdef uint8_t marker_type cdef uint8_t marker_type

View File

@ -30,4 +30,4 @@
# file doc/src/conf.py both reference this file directly. # file doc/src/conf.py both reference this file directly.
#------------------------------------------------------------------------------ #------------------------------------------------------------------------------
__version__ = "1.3.1" __version__ = "1.3.2"

View File

@ -138,7 +138,8 @@ class TestCase(test_env.BaseTestCase):
def test_1105_bad_connect_string(self): def test_1105_bad_connect_string(self):
"1105 - connection to database with bad connect string" "1105 - connection to database with bad connect string"
self.assertRaisesRegex(oracledb.DatabaseError, "^DPY-4001:|ORA-12547:", self.assertRaisesRegex(oracledb.DatabaseError,
"^DPY-4000:|^DPY-4026:|^DPY-4027:|ORA-12154:",
oracledb.connect, test_env.get_main_user()) oracledb.connect, test_env.get_main_user())
self.assertRaisesRegex(oracledb.DatabaseError, "^DPY-4000:|^DPY-4001:", self.assertRaisesRegex(oracledb.DatabaseError, "^DPY-4000:|^DPY-4001:",
oracledb.connect, test_env.get_main_user() + \ oracledb.connect, test_env.get_main_user() + \

View File

@ -138,8 +138,8 @@ class TestCase(test_env.BaseTestCase):
rows = ref_cursor.fetchall() rows = ref_cursor.fetchall()
self.assertEqual(rows, expected_value) self.assertEqual(rows, expected_value)
def test_1306_refcursor_prefetchrows(self): def test_1306_refcursor_round_trips(self):
"1306 - test prefetch rows and arraysize using a refcursor" "1306 - test round trips using a REF cursor"
self.setup_round_trip_checker() self.setup_round_trip_checker()
# simple DDL only requires a single round trip # simple DDL only requires a single round trip
@ -155,14 +155,23 @@ class TestCase(test_env.BaseTestCase):
cursor.executemany(sql, data) cursor.executemany(sql, data)
self.assertRoundTrips(1) self.assertRoundTrips(1)
# create refcursor and execute stored procedure # create REF cursor and execute stored procedure
# (array size set before procedure is called)
with self.connection.cursor() as cursor: with self.connection.cursor() as cursor:
refcursor = self.connection.cursor() refcursor = self.connection.cursor()
refcursor.prefetchrows = 150 refcursor.arraysize = 150
refcursor.arraysize = 50
cursor.callproc("myrefcursorproc", [refcursor]) cursor.callproc("myrefcursorproc", [refcursor])
refcursor.fetchall() refcursor.fetchall()
self.assertRoundTrips(4) self.assertRoundTrips(5)
# create REF cursor and execute stored procedure
# (array size set after procedure is called)
with self.connection.cursor() as cursor:
refcursor = self.connection.cursor()
cursor.callproc("myrefcursorproc", [refcursor])
refcursor.arraysize = 145
refcursor.fetchall()
self.assertRoundTrips(6)
def test_1307_refcursor_execute_different_sql(self): def test_1307_refcursor_execute_different_sql(self):
"1307 - test executing different SQL after getting a REF cursor" "1307 - test executing different SQL after getting a REF cursor"

View File

@ -1,5 +1,5 @@
#------------------------------------------------------------------------------ #------------------------------------------------------------------------------
# Copyright (c) 2020, 2022, Oracle and/or its affiliates. # Copyright (c) 2020, 2023, Oracle and/or its affiliates.
# #
# This software is dual-licensed to you under the Universal Permissive License # This software is dual-licensed to you under the Universal Permissive License
# (UPL) 1.0 as shown at https://oss.oracle.com/licenses/upl and Apache License # (UPL) 1.0 as shown at https://oss.oracle.com/licenses/upl and Apache License
@ -310,5 +310,32 @@ class TestCase(test_env.BaseTestCase):
fetched_data = self.cursor.fetchall() fetched_data = self.cursor.fetchall()
self.assertEqual(data, fetched_data) self.assertEqual(data, fetched_data)
def test_4022_plsql_large_number_of_binds(self):
"4022 - test PL/SQL with a lerge number of binds"
parts = []
bind_names = []
all_bind_values = []
out_binds = []
for i in range(5):
all_bind_values.append([])
for i in range(350):
n = len(parts) + 1
bind_names.extend([f"v_out_{n}_0", f"a_{n}", f"b_{n}", f"c_{n}"])
parts.append(f":v_out{n} := :a_{n} + :b_{n} + :c_{n};")
out_binds.append(self.cursor.var(int,
arraysize=len(all_bind_values)))
for j, bind_values in enumerate(all_bind_values):
bind_values.extend([out_binds[-1], n * 1 + j, n * 2 + j,
n * 3 + j])
lf = "\n"
sql = f"begin{lf}{lf.join(parts)}{lf}end;"
self.cursor.executemany(sql, all_bind_values)
init_val = 6
for var in out_binds:
expected = [init_val, init_val + 3, init_val + 6, init_val + 9,
init_val + 12]
self.assertEqual(var.values, expected)
init_val += 6
if __name__ == "__main__": if __name__ == "__main__":
test_env.run_test_cases() test_env.run_test_cases()

View File

@ -346,13 +346,15 @@ class TestCase(test_env.BaseTestCase):
num_rows = 590 num_rows = 590
with self.connection.cursor() as cursor: with self.connection.cursor() as cursor:
cursor.execute("truncate table TestTempTable") cursor.execute("truncate table TestTempTable")
self.assertRoundTrips(1)
sql = "insert into TestTempTable (IntCol) values (:1)" sql = "insert into TestTempTable (IntCol) values (:1)"
data = [(n + 1,) for n in range(num_rows)] data = [(n + 1,) for n in range(num_rows)]
cursor.executemany(sql, data) cursor.executemany(sql, data)
cursor.prefetchrows = 300 self.assertRoundTrips(1)
cursor.arraysize = 300 cursor.prefetchrows = 30
cursor.arraysize = 100
cursor.execute("select IntCol from TestTempTable").fetchall() cursor.execute("select IntCol from TestTempTable").fetchall()
self.assertRoundTrips(4) self.assertRoundTrips(7)
def test_4324_bind_names_with_single_line_comments(self): def test_4324_bind_names_with_single_line_comments(self):
"4324 - test bindnames() with single line comments" "4324 - test bindnames() with single line comments"

View File

@ -619,5 +619,14 @@ class TestCase(test_env.BaseTestCase):
self.assertEqual(password, None) self.assertEqual(password, None)
self.assertEqual(dsn, None) self.assertEqual(dsn, None)
def test_4561_dsn_with_no_credentials(self):
"4561 - test parsing a DSN with no credentials"
dsn_in = "my_alias_4561"
params = oracledb.ConnectParams()
user, password, dsn_out = params.parse_dsn_with_credentials(dsn_in)
self.assertEqual(user, None)
self.assertEqual(password, None)
self.assertEqual(dsn_out, dsn_in)
if __name__ == "__main__": if __name__ == "__main__":
test_env.run_test_cases() test_env.run_test_cases()

View File

@ -1,5 +1,5 @@
#------------------------------------------------------------------------------ #------------------------------------------------------------------------------
# Copyright (c) 2020, 2022, Oracle and/or its affiliates. # Copyright (c) 2020, 2023, Oracle and/or its affiliates.
# #
# This software is dual-licensed to you under the Universal Permissive License # This software is dual-licensed to you under the Universal Permissive License
# (UPL) 1.0 as shown at https://oss.oracle.com/licenses/upl and Apache License # (UPL) 1.0 as shown at https://oss.oracle.com/licenses/upl and Apache License