Support for enhanced metadata.

This commit is contained in:
Anthony Tuininga 2023-08-10 15:10:33 -06:00
parent 6646529ec5
commit 6caccf9b51
38 changed files with 560 additions and 271 deletions

View File

@ -756,14 +756,19 @@ Connection Attributes
This read-write attribute specifies a method called for each column that is
going to be fetched from any cursor associated with this connection. The
method signature is handler(cursor, name, defaultType, length, precision,
scale) and the return value is expected to be a variable object or None in
which case a default variable object will be created. If this attribute is
None, the default behavior will take place for all columns fetched from
cursors.
method signature is ``handler(cursor, metadata)`` and the return value is
expected to be a :ref:`variable object<varobj>` or None in which case a
default variable object will be created. If this attribute is None, the
default behavior will take place for all columns fetched from cursors.
See :ref:`outputtypehandlers`.
.. versionchanged:: 1.4
The method signature was changed. The previous signature
``handler(cursor, name, default_type, length, precision, scale)`` will
still work but is deprecated and will be removed in a future version.
.. note::
This attribute is an extension to the DB API definition.

View File

@ -496,14 +496,10 @@ Cursor Attributes
.. data:: Cursor.description
This read-only attribute is a sequence of 7-item sequences. Each of these
sequences contains information describing one result column: (name, type,
display_size, internal_size, precision, scale, null_ok). This attribute
will be None for operations that do not return rows or if the cursor has
not had an operation invoked via the :meth:`~Cursor.execute()` method yet.
The type will be one of the :ref:`database type constants <dbtypes>`
defined at the module level.
This read-only attribute is a sequence of :ref:`FetchInfo<fetchinfoobj>`
objects. This attribute will be None for operations that do not return rows
or if the cursor has not had an operation invoked via the
:meth:`~Cursor.execute()` method yet.
.. attribute:: Cursor.fetchvars
@ -541,13 +537,19 @@ Cursor Attributes
This read-write attribute specifies a method called for each column that is
to be fetched from this cursor. The method signature is
handler(cursor, name, defaultType, length, precision, scale) and the return
value is expected to be a variable object or None in which case a default
variable object will be created. If this attribute is None, then the default
handler(cursor, metadata) and the return value is expected to be a
:ref:`variable object<varobj>` or None in which case a default variable
object will be created. If this attribute is None, then the default
behavior will take place for all columns fetched from this cursor.
See :ref:`outputtypehandlers`.
.. versionchanged:: 1.4
The method signature was changed. The previous signature
handler(cursor, name, default_type, length, precision, scale) will
still work but is deprecated and will be removed in a future version.
.. note::
This attribute is an extension to the DB API definition.

View File

@ -9,6 +9,19 @@ when they were first deprecated and a comment on what should be used instead,
if applicable. The most recent deprecations are listed first.
.. list-table-with-summary:: Deprecated in python-oracledb 1.4
:header-rows: 1
:class: wy-table-responsive
:summary: The first column, Name, displays the deprecated API name. The second column, Comments, includes information about when the API was deprecated and what API to use, if applicable.
:name: _deprecations_1_4
* - Name
- Comments
* - Output type handler with arguments
``handler(cursor, name, default_type, length, precision, scale)``
- Replace with ``handler(cursor, metadata)``. See
:ref:`outputtypehandlers`.
.. list-table-with-summary:: Deprecated in python-oracledb 1.0
:header-rows: 1
:class: wy-table-responsive

View File

@ -0,0 +1,71 @@
.. _fetchinfoobj:
**********************
API: FetchInfo Objects
**********************
These objects are created internally when a query is executed. They are found
in the sequence :data:`Cursor.description`. For compatibility with the Python
Database API, this object behaves as a 7-tuple containing the values for the
attributes ``name``, ``type_code``, ``display_size``, ``internal_size``,
``precision``, ``scale``, and ``null_ok`` in that order. For example, if
``fetch_info`` is of type FetchInfo, then ``fetch_info[2]`` is the same as
``fetch_info.display_size``.
.. note::
This object is an extension the DB API.
FetchInfo Attributes
====================
.. attribute:: FetchInfo.display_size
This read-only attribute returns the display size of the column as mandated
by the Python Database API.
.. attribute:: FetchInfo.internal_size
This read-only attribute returns the internal size of the column as
mandated by the Python Database API.
.. attribute:: FetchInfo.is_json
This read-only attribute returns whether the column is known to contain
JSON data. This will be ``true`` when the type code is
``oracledb.DB_TYPE_JSON`` as well as when an "IS JSON" constraint is
enabled on LOB and VARCHAR2 columns.
.. attribute:: FetchInfo.name
This read-only attribute returns the name of the column as mandated by the
Python Database API.
.. attribute:: FetchInfo.null_ok
This read-only attribute returns whether nulls are allowed in the column as
mandated by the Python Database API.
.. attribute:: FetchInfo.precision
This read-only attribute returns the precision of the column as mandated by
the Python Database API.
.. attribute:: FetchInfo.scale
This read-only attribute returns the scale of the column as mandated by
the Python Database API.
.. attribute:: FetchInfo.type
This read-only attribute returns the type of the column. This will be an
:ref:`Oracle Object Type <dbobjecttype>` if the column contains Oracle
objects; otherwise, it will be one of the :ref:`database type constants
<dbtypes>` defined at the module level.
.. attribute:: FetchInfo.type_code
This read-only attribute returns the type of the column as mandated by the
Python Database API. The type will be one of the :ref:`database type
constants <dbtypes>` defined at the module level.

View File

@ -66,6 +66,7 @@ API Manual
api_manual/connection_pool.rst
api_manual/pool_params.rst
api_manual/cursor.rst
api_manual/fetch_info.rst
api_manual/variable.rst
api_manual/subscription.rst
api_manual/lob.rst

View File

@ -70,6 +70,15 @@ Thick Mode Changes
Common Changes
++++++++++++++
#) Replaced fixed 7-tuple for the cursor metadata found in
:data:`Cursor.description` with a class which provides additional
information such as the database object type and whether the column
contains JSON data.
#) Changed the signature for output type handlers to
``handler(cursor, metadata)`` where the ``metadata`` parameter is a
:ref:`FetchInfo<fetchinfoobj>` object containing the same information found
in :data:`Cursor.description`. The original signature for output type
handlers is deprecated and will be removed in some future version.
#) Added support for fetching VARCHAR2 and LOB columns which contain JSON (and
have the "IS JSON" check constraint enabled) in the same way as columns of
type JSON (which requires Oracle Database 21c or higher) are fetched. In

View File

@ -133,16 +133,16 @@ To convert numbers:
locale.setlocale(locale.LC_ALL, 'de_DE.UTF-8')
# simple naive conversion
def type_handler1(cursor, name, default_type, size, precision, scale):
if default_type == oracledb.DB_TYPE_NUMBER:
def type_handler1(cursor, metadata):
if metadata.type_code is oracledb.DB_TYPE_NUMBER:
return cursor.var(oracledb.DB_TYPE_VARCHAR, arraysize=cursor.arraysize,
outconverter=lambda v: v.replace('.', ','))
outconverter=lambda v: v.replace('.', ','))
# locale conversion
def type_handler2(cursor, name, default_type, size, precision, scale):
if default_type == oracledb.DB_TYPE_NUMBER:
return cursor.var(default_type, arraysize=cursor.arraysize,
outconverter=lambda v: locale.format_string("%g", v))
def type_handler2(cursor, metadata):
if metadata.type_code is oracledb.DB_TYPE_NUMBER:
return cursor.var(metadata.type_code, arraysize=cursor.arraysize,
outconverter=lambda v: locale.format_string("%g", v))
connection = oracledb.connect(user="hr", password=userpwd,
@ -186,16 +186,16 @@ To convert dates:
locale_date_format = locale.nl_langinfo(locale.D_T_FMT)
# simple naive conversion
def type_handler3(cursor, name, default_type, size, precision, scale):
if default_type == oracledb.DB_TYPE_DATE:
return cursor.var(default_type, arraysize=cursor.arraysize,
outconverter=lambda v: v.strftime("%Y-%m-%d %H:%M:%S"))
def type_handler3(cursor, metadata):
if metadata.type_code is oracledb.DB_TYPE_DATE:
return cursor.var(metadata.type_code, arraysize=cursor.arraysize,
outconverter=lambda v: v.strftime("%Y-%m-%d %H:%M:%S"))
# locale conversion
def type_handler4(cursor, name, default_type, size, precision, scale):
if default_type == oracledb.DB_TYPE_DATE:
return cursor.var(default_type, arraysize=cursor.arraysize,
outconverter=lambda v: v.strftime(locale_date_format))
if metadata.type_code is oracledb.DB_TYPE_DATE:
return cursor.var(metadata.type_code, arraysize=cursor.arraysize,
outconverter=lambda v: v.strftime(locale_date_format))
connection = oracledb.connect(user="hr", password=userpwd,

View File

@ -107,12 +107,12 @@ handler:
.. code-block:: python
def output_type_handler(cursor, name, default_type, size, precision, scale):
if default_type == oracledb.DB_TYPE_CLOB:
def output_type_handler(cursor, metadata):
if metadata.type_code is oracledb.DB_TYPE_CLOB:
return cursor.var(oracledb.DB_TYPE_LONG, arraysize=cursor.arraysize)
if default_type == oracledb.DB_TYPE_BLOB:
if metadata.type_code is oracledb.DB_TYPE_BLOB:
return cursor.var(oracledb.DB_TYPE_LONG_RAW, arraysize=cursor.arraysize)
if default_type == oracledb.DB_TYPE_NCLOB:
if metadata.type_code is oracledb.DB_TYPE_NCLOB:
return cursor.var(oracledb.DB_TYPE_LONG_NVARCHAR, arraysize=cursor.arraysize)
connection.outputtypehandler = output_type_handler

View File

@ -312,10 +312,10 @@ cursors created by that connection will have their fetch type handling changed.
The output type handler is expected to be a function with the following
signature::
handler(cursor, name, default_type, size, precision, scale)
handler(cursor, metadata)
The parameters are the same information as the query column metadata found in
:attr:`Cursor.description`.
The metadata parameter is a :ref:`FetchInfo object<fetchinfoobj>`, which is the
same value found in :attr:`Cursor.description`.
The function is called once for each column that is going to be
fetched. The function is expected to return a :ref:`variable object <varobj>`
@ -326,8 +326,8 @@ For example:
.. code-block:: python
def output_type_handler(cursor, name, default_type, size, precision, scale):
if default_type == oracledb.DB_TYPE_NUMBER:
def output_type_handler(cursor, metadata):
if metadata.type_code is oracledb.DB_TYPE_NUMBER:
return cursor.var(oracledb.DB_TYPE_VARCHAR, arraysize=cursor.arraysize)
This output type handler is called once for each column in the SELECT query.
@ -375,7 +375,7 @@ For example:
.. code-block:: python
def output_type_handler(cursor, name, default_type, size, precision, scale):
def output_type_handler(cursor, metadata):
def out_converter(d):
if isinstance(d, str):
@ -383,7 +383,7 @@ For example:
else:
return f"{d} was not a string"
if default_type == oracledb.DB_TYPE_NUMBER:
if metadata.type_code is oracledb.DB_TYPE_NUMBER:
return cursor.var(oracledb.DB_TYPE_VARCHAR,
arraysize=cursor.arraysize, outconverter=out_converter)
@ -456,7 +456,7 @@ An example showing an :ref:`output type handler <outputtypehandlers>`, an
.. code-block:: python
def output_type_handler(cursor, name, default_type, size, precision, scale):
def output_type_handler(cursor, metadata):
def out_converter(d):
if type(d) is str:
@ -464,7 +464,7 @@ An example showing an :ref:`output type handler <outputtypehandlers>`, an
else:
return f"{d} was not a string"
if default_type == oracledb.DB_TYPE_NUMBER:
if metadata.type_code is oracledb.DB_TYPE_NUMBER:
return cursor.var(oracledb.DB_TYPE_VARCHAR,
arraysize=cursor.arraysize, outconverter=out_converter)
@ -532,8 +532,8 @@ to use an :ref:`output type handler <outputtypehandlers>` do the conversion.
import decimal
def number_to_decimal(cursor, name, default_type, size, precision, scale):
if default_type == oracledb.DB_TYPE_NUMBER:
def number_to_decimal(cursor, metadata):
if metadata.type_code is oracledb.DB_TYPE_NUMBER:
return cursor.var(decimal.Decimal, arraysize=cursor.arraysize)
cursor.outputtypehandler = number_to_decimal
@ -824,9 +824,8 @@ The following sample demonstrates how to use this feature:
.. code-block:: python
# define output type handler
def return_strings_as_bytes(cursor, name, default_type, size,
precision, scale):
if default_type == oracledb.DB_TYPE_VARCHAR:
def return_strings_as_bytes(cursor, metadata):
if metadata.type_code is oracledb.DB_TYPE_VARCHAR:
return cursor.var(str, arraysize=cursor.arraysize,
bypass_decode=True)
@ -900,9 +899,10 @@ columns:
.. code-block:: python
def output_type_handler(cursor, name, default_type, size, precision, scale):
if default_type == oracledb.DB_TYPE_VARCHAR:
return cursor.var(default_type, size, arraysize=cursor.arraysize,
def output_type_handler(cursor, metadata):
if metadata.type_code is oracledb.DB_TYPE_VARCHAR:
return cursor.var(metadata.type_code, size,
arraysize=cursor.arraysize,
encoding_errors="replace")
cursor.outputtypehandler = output_type_handler

View File

@ -1,5 +1,5 @@
#------------------------------------------------------------------------------
# Copyright (c) 2016, 2022, Oracle and/or its affiliates.
# Copyright (c) 2016, 2023, Oracle and/or its affiliates.
#
# 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
@ -52,7 +52,7 @@ class Cursor(oracledb.Cursor):
if prepare_needed:
description = self.description
if description is not None:
names = [d[0] for d in description]
names = [d.name for d in description]
self.rowfactory = collections.namedtuple("GenericQuery", names)
return result

View File

@ -1,5 +1,5 @@
#------------------------------------------------------------------------------
# Copyright (c) 2016, 2022, Oracle and/or its affiliates.
# Copyright (c) 2016, 2023, Oracle and/or its affiliates.
#
# 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
@ -72,7 +72,7 @@ with connection.cursor() as cursor:
print("Fetch each row as a Dictionary")
cursor.execute(sql)
columns = [col[0] for col in cursor.description]
columns = [col.name for col in cursor.description]
cursor.rowfactory = lambda *args: dict(zip(columns, args))
for row in cursor:
print(row)

View File

@ -1,5 +1,5 @@
# -----------------------------------------------------------------------------
# Copyright (c) 2021, 2022, Oracle and/or its affiliates.
# Copyright (c) 2021, 2023, Oracle and/or its affiliates.
#
# 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
@ -39,9 +39,8 @@ if not sample_env.get_is_thin():
STRING_VAL = 'I bought a cafetière on the Champs-Élysées'
def return_strings_as_bytes(cursor, name, default_type, size, precision,
scale):
if default_type == oracledb.DB_TYPE_VARCHAR:
def return_strings_as_bytes(cursor, metadata):
if metadata.type_code is oracledb.DB_TYPE_VARCHAR:
return cursor.var(str, arraysize=cursor.arraysize, bypass_decode=True)
connection = oracledb.connect(user=sample_env.get_main_user(),

View File

@ -1,5 +1,5 @@
#------------------------------------------------------------------------------
# Copyright (c) 2018, 2022, Oracle and/or its affiliates.
# Copyright (c) 2018, 2023, Oracle and/or its affiliates.
#
# 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
@ -65,12 +65,9 @@ cursor = connection.cursor()
# executed for a single transaction
connection.autocommit = True
# define output type handler to fetch LOBs, avoiding the second round trip to
# the database to read the LOB contents
def output_type_handler(cursor, name, default_type, size, precision, scale):
if default_type == oracledb.BLOB:
return cursor.var(oracledb.LONG_BINARY, arraysize=cursor.arraysize)
connection.outputtypehandler = output_type_handler
# do not fetch LOBs, avoiding the second round trip to the database to read the
# LOB contents
oracledb.defaults.fetch_lobs = False
# drop and create table
print("Dropping and creating table...")

View File

@ -1315,8 +1315,8 @@ for row in cur.execute("select * from dept"):
<p>Add an output type handler to the bottom of the file:</p>
<pre>
<strong>def ReturnNumbersAsStrings(cursor, name, defaultType, size, precision, scale):
if defaultType == oracledb.NUMBER:
<strong>def ReturnNumbersAsStrings(cursor, metadata):
if metdata.type_code is oracledb.DB_TYPE_NUMBER:
return cursor.var(str, 9, cursor.arraysize)
print("Output type handler output...")
@ -1376,8 +1376,8 @@ import db_config
con = oracledb.connect(user=db_config.user, password=db_config.pw, dsn=db_config.dsn)
cur = con.cursor()
<strong>def ReturnNumbersAsDecimal(cursor, name, defaultType, size, precision, scale):
if defaultType == oracledb.NUMBER:
<strong>def ReturnNumbersAsDecimal(cursor, metadata):
if metadata.type_code is oracledb.NUMBER:
return cursor.var(str, 9, cursor.arraysize, outconverter=decimal.Decimal)
cur.outputtypehandler = ReturnNumbersAsDecimal</strong>
@ -2228,10 +2228,9 @@ def SDOInputTypeHandler(cursor, value, numElements):
return mySDO(int(DBobj.SDO_GTYPE), DBobj.SDO_ELEM_INFO.aslist(),
DBobj.SDO_ORDINATES.aslist())</strong>
<strong>def SDOOutputTypeHandler(cursor, name, default_type, size, precision,
scale):
if default_type == oracledb.DB_TYPE_OBJECT:
return cursor.var(obj_type, arraysize=cursor.arraysize,
<strong>def SDOOutputTypeHandler(cursor, metadata):
if metadata.type_code is oracledb.DB_TYPE_OBJECT:
return cursor.var(metadata.type, arraysize=cursor.arraysize,
outconverter=SDOOutConverter)</strong>
sdo = mySDO(2003, [1, 1003, 3], [1, 1, 5, 7]) # Python object

View File

@ -1,5 +1,5 @@
# -----------------------------------------------------------------------------
# Copyright (c) 2022, Oracle and/or its affiliates.
# Copyright (c) 2022, 2023, Oracle and/or its affiliates.
#
# 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
@ -79,9 +79,9 @@ def input_type_handler(cursor, value, num_elements):
inconverter=building_in_converter)
def output_type_handler(cursor, name, default_type, size, precision, scale):
if default_type == oracledb.STRING:
return cursor.var(default_type, arraysize=cursor.arraysize,
def output_type_handler(cursor, metadata):
if metadata.type_code is oracledb.STRING:
return cursor.var(metadata.type_code, arraysize=cursor.arraysize,
outconverter=Building.from_json)
connection = oracledb.connect(user=sample_env.get_main_user(),

View File

@ -1,5 +1,5 @@
#------------------------------------------------------------------------------
# Copyright (c) 2016, 2022, Oracle and/or its affiliates.
# Copyright (c) 2016, 2023, Oracle and/or its affiliates.
#
# Portions Copyright 2007-2015, Anthony Tuininga. All rights reserved.
#
@ -82,9 +82,9 @@ def input_type_handler(cursor, value, num_elements):
return cursor.var(obj_type, arraysize=num_elements,
inconverter=building_in_converter)
def output_type_handler(cursor, name, default_type, size, precision, scale):
if default_type == oracledb.OBJECT:
return cursor.var(obj_type, arraysize=cursor.arraysize,
def output_type_handler(cursor, metadata):
if metadata.type_code is oracledb.DB_TYPE_OBJECT:
return cursor.var(metadata.type, arraysize=cursor.arraysize,
outconverter=building_out_converter)
buildings = [

View File

@ -76,6 +76,10 @@ from .dbobject import (
DbObjectType as DbObjectType
)
from .fetch_info import (
FetchInfo as FetchInfo
)
from .var import (
Var as Var
)
@ -158,6 +162,7 @@ del package
del exceptions, errors, connection, pool, constants, driver_mode, sys
del constructors, dsn, lob, base_impl, thick_impl, thin_impl, utils, var
del connect_params, pool_params, subscr, aq, soda, cursor, dbobject, future
del fetch_info
# general aliases (for backwards compatibility)
ObjectType = DbObjectType

View File

@ -268,6 +268,7 @@ cdef class BaseCursorImpl:
public object outputtypehandler
public object rowfactory
public bint scrollable
public list fetch_info_impls
public list fetch_vars
public list fetch_var_impls
public list bind_vars
@ -291,14 +292,14 @@ cdef class BaseCursorImpl:
bint defer_type_assignment) except -1
cdef int _close(self, bint in_del) except -1
cdef int _create_fetch_var(self, object conn, object cursor,
object type_handler, ssize_t pos,
FetchInfo fetch_info) except -1
object type_handler, bint uses_fetch_info,
ssize_t pos, FetchInfoImpl fetch_info) except -1
cdef object _create_row(self)
cdef BaseVarImpl _create_var_impl(self, object conn)
cdef int _fetch_rows(self, object cursor) except -1
cdef BaseConnImpl _get_conn_impl(self)
cdef object _get_input_type_handler(self)
cdef object _get_output_type_handler(self)
cdef object _get_output_type_handler(self, bint* uses_fetch_info)
cdef int _init_fetch_vars(self, uint32_t num_columns) except -1
cdef bint _is_plsql(self)
cdef int _perform_binds(self, object conn, uint32_t num_execs) except -1
@ -306,17 +307,17 @@ cdef class BaseCursorImpl:
cdef int _verify_var(self, object var) except -1
cdef class FetchInfo:
cdef class FetchInfoImpl:
cdef:
int16_t _precision
int16_t _scale
uint32_t _buffer_size
uint32_t _size
bint _nulls_allowed
str _name
DbType _dbtype
BaseDbObjectTypeImpl _objtype
bint _is_json_col
readonly int16_t precision
readonly int16_t scale
readonly uint32_t buffer_size
readonly uint32_t size
readonly bint nulls_allowed
readonly str name
readonly DbType dbtype
readonly BaseDbObjectTypeImpl objtype
readonly bint is_json
cdef class BaseVarImpl:
@ -337,7 +338,7 @@ cdef class BaseVarImpl:
readonly BaseDbObjectTypeImpl objtype
BaseConnImpl _conn_impl
int _preferred_num_type
FetchInfo _fetch_info
FetchInfoImpl _fetch_info
bint _is_value_set
cdef int _bind(self, object conn, BaseCursorImpl 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
# (UPL) 1.0 as shown at https://oss.oracle.com/licenses/upl and Apache License
@ -41,6 +41,7 @@ from libc.stdint cimport uint8_t, uint16_t, uint32_t, uint64_t
import base64
import datetime
import decimal
import inspect
import json
import os
import re
@ -65,6 +66,7 @@ cdef type PY_TYPE_DB_OBJECT_TYPE
cdef type PY_TYPE_LOB
cdef type PY_TYPE_TIMEDELTA = datetime.timedelta
cdef type PY_TYPE_VAR
cdef type PY_TYPE_FETCHINFO
cdef int32_t* INTEGRITY_ERROR_CODES = [
1, # unique constraint violated

View File

@ -35,6 +35,7 @@ from . import __name__ as MODULE_NAME
from . import errors, exceptions
from . import connection as connection_module
from .defaults import defaults
from .fetch_info import FetchInfo
from .var import Var
from .base_impl import DbType, DB_TYPE_OBJECT
from .dbobject import DbObjectType
@ -52,6 +53,7 @@ class Cursor:
self._impl.scrollable = scrollable
self._impl.arraysize = defaults.arraysize
self._impl.prefetchrows = defaults.prefetchrows
self._fetch_infos = None
def __del__(self):
if self._impl is not None:
@ -137,6 +139,7 @@ class Cursor:
self._impl.prepare(statement, tag, cache_statement)
self.statement = statement
self._impl.rowfactory = None
self._fetch_infos = None
def _set_oci_attr(self, attr_num: int, attr_type: int,
value: Any) -> None:
@ -304,8 +307,10 @@ class Cursor:
cursor has not had an operation invoked via the execute() method yet.
"""
self._verify_open()
if self._impl.is_query(self):
return self._impl.get_description()
if self._fetch_infos is None and self._impl.is_query(self):
self._fetch_infos = [FetchInfo._from_impl(i) \
for i in self._impl.fetch_info_impls]
return self._fetch_infos
def execute(self, statement: Union[str, None],
parameters: Union[list, tuple, dict]=None,

184
src/oracledb/fetch_info.py Normal file
View File

@ -0,0 +1,184 @@
#------------------------------------------------------------------------------
# Copyright (c) 2023, Oracle and/or its affiliates.
#
# 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
# 2.0 as shown at http://www.apache.org/licenses/LICENSE-2.0. You may choose
# either license.
#
# If you elect to accept the software under the Apache License, Version 2.0,
# the following applies:
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# https://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#------------------------------------------------------------------------------
#------------------------------------------------------------------------------
# fetch_info.py
#
# Contains the FetchInfo class which stores metadata about columns that are
# being fetched.
#------------------------------------------------------------------------------
from typing import Union
from . import __name__ as MODULE_NAME
from .dbobject import DbObjectType
from .base_impl import (
DbType,
DB_TYPE_DATE,
DB_TYPE_TIMESTAMP,
DB_TYPE_TIMESTAMP_LTZ,
DB_TYPE_TIMESTAMP_TZ,
DB_TYPE_BINARY_FLOAT,
DB_TYPE_BINARY_DOUBLE,
DB_TYPE_BINARY_INTEGER,
DB_TYPE_NUMBER
)
class FetchInfo:
"""
Identifies metadata of columns that are being fetched.
"""
__module__ = MODULE_NAME
def __eq__(self, other):
return tuple(self) == other
def __getitem__(self, index):
"""
Return the parts mandated by the Python Database API.
"""
if index == 0 or index == -7:
return self.name
elif index == 1 or index == -6:
return self.type_code
elif index == 2 or index == -5:
return self.display_size
elif index == 3 or index == -4:
return self.internal_size
elif index == 4 or index == -3:
return self.precision
elif index == 5 or index == -2:
return self.scale
elif index == 6 or index == -1:
return self.null_ok
raise IndexError("list index out of range")
def __len__(self):
"""
Length mandated by the Python Database API.
"""
return 7
def __repr__(self):
return repr(tuple(self))
def __str__(self):
return str(tuple(self))
@classmethod
def _from_impl(cls, impl):
info = cls.__new__(cls)
info._impl = impl
info._type = None
return info
@property
def display_size(self) -> Union[int, None]:
"""
Returns the display size of the column.
"""
if self._impl.size > 0:
return self._impl.size
dbtype = self._impl.dbtype
if dbtype is DB_TYPE_DATE \
or dbtype is DB_TYPE_TIMESTAMP \
or dbtype is DB_TYPE_TIMESTAMP_LTZ \
or dbtype is DB_TYPE_TIMESTAMP_TZ:
return 23
elif dbtype is DB_TYPE_BINARY_FLOAT \
or dbtype is DB_TYPE_BINARY_DOUBLE \
or dbtype is DB_TYPE_BINARY_INTEGER \
or dbtype is DB_TYPE_NUMBER:
if self._impl.precision:
display_size = self._impl.precision + 1
if self._impl.scale > 0:
display_size += self._impl.scale + 1
else:
display_size = 127
return display_size
@property
def internal_size(self) -> Union[int, None]:
"""
Returns the size in bytes of the column.
"""
if self._impl.size > 0:
return self._impl.buffer_size
@property
def is_json(self) -> bool:
"""
Returns whether the column contains JSON.
"""
return self._impl.is_json
@property
def name(self) -> str:
"""
Returns the name of the column.
"""
return self._impl.name
@property
def null_ok(self) -> bool:
"""
Returns whether nulls or permitted or not in the column.
"""
return self._impl.nulls_allowed
@property
def precision(self) -> Union[int, None]:
"""
Returns the precision of the column.
"""
if self._impl.precision or self._impl.scale:
return self._impl.precision
@property
def scale(self) -> Union[int, None]:
"""
Returns the scale of the column.
"""
if self._impl.precision or self._impl.scale:
return self._impl.scale
@property
def type(self) -> Union[DbType, DbObjectType]:
"""
Returns the type of the column, as either a database object type or a
database type.
"""
if self._type is None:
if self._impl.objtype is not None:
self._type = DbObjectType._from_impl(self._impl.objtype)
else:
self._type = self._impl.dbtype
return self._type
@property
def type_code(self) -> DbType:
"""
Returns the type of the column.
"""
return self._impl.dbtype

View File

@ -29,6 +29,9 @@
# base_impl.pyx).
#------------------------------------------------------------------------------
cdef class FetchInfoImpl:
pass
cdef class BaseCursorImpl:
@cython.boundscheck(False)
@ -131,26 +134,35 @@ cdef class BaseCursorImpl:
@cython.boundscheck(False)
@cython.wraparound(False)
cdef int _create_fetch_var(self, object conn, object cursor,
object type_handler, ssize_t pos,
FetchInfo fetch_info) except -1:
object type_handler, bint uses_fetch_info,
ssize_t pos,
FetchInfoImpl fetch_info) except -1:
"""
Create the fetch variable for the given position and fetch information.
The output type handler is consulted, if present, to make any necessary
adjustments.
"""
cdef:
object var, pub_fetch_info
BaseVarImpl var_impl
uint32_t db_type_num
object var
# add the fetch info to the list used for handling the cursor
# description attribute
self.fetch_info_impls[pos] = fetch_info
# if an output type handler is specified, call it; the output type
# handler should return a variable or None; the value None implies that
# the default processing should take place just as if no output type
# handler was defined
if type_handler is not None:
var = type_handler(cursor, fetch_info._name, fetch_info._dbtype,
fetch_info._size, fetch_info._precision,
fetch_info._scale)
if uses_fetch_info:
pub_fetch_info = PY_TYPE_FETCHINFO._from_impl(fetch_info)
var = type_handler(cursor, pub_fetch_info)
else:
var = type_handler(cursor, fetch_info.name, fetch_info.dbtype,
fetch_info.size, fetch_info.precision,
fetch_info.scale)
if var is not None:
self._verify_var(var)
var_impl = var._impl
@ -165,13 +177,13 @@ cdef class BaseCursorImpl:
# otherwise, create a new variable using the provided fetch information
var_impl = self._create_var_impl(conn)
var_impl.num_elements = self._fetch_array_size
var_impl.dbtype = fetch_info._dbtype
var_impl.objtype = fetch_info._objtype
var_impl.name = fetch_info._name
var_impl.size = fetch_info._size
var_impl.precision = fetch_info._precision
var_impl.scale = fetch_info._scale
var_impl.nulls_allowed = fetch_info._nulls_allowed
var_impl.dbtype = fetch_info.dbtype
var_impl.objtype = fetch_info.objtype
var_impl.name = fetch_info.name
var_impl.size = fetch_info.size
var_impl.precision = fetch_info.precision
var_impl.scale = fetch_info.scale
var_impl.nulls_allowed = fetch_info.nulls_allowed
var_impl._fetch_info = fetch_info
# adjust the variable based on the defaults specified by the user, if
@ -183,7 +195,7 @@ cdef class BaseCursorImpl:
elif var_impl.scale == 0 \
or (var_impl.scale == -127 and var_impl.precision == 0):
var_impl._preferred_num_type = NUM_TYPE_INT
elif future.old_json_col_as_obj and fetch_info._is_json_col \
elif future.old_json_col_as_obj and fetch_info.is_json \
and db_type_num != DB_TYPE_NUM_JSON:
def converter(value):
if isinstance(value, PY_TYPE_LOB):
@ -264,23 +276,34 @@ cdef class BaseCursorImpl:
def _get_oci_attr(self, uint32_t attr_num, uint32_t attr_type):
pass
cdef object _get_output_type_handler(self):
cdef object _get_output_type_handler(self, bint *uses_fetch_info):
"""
Return the output type handler to use for the cursor. If one is not
directly defined on the cursor then the one defined on the connection
is used instead.
"""
cdef BaseConnImpl conn_impl
cdef:
BaseConnImpl conn_impl
object type_handler
if self.outputtypehandler is not None:
return self.outputtypehandler
conn_impl = self._get_conn_impl()
return conn_impl.outputtypehandler
type_handler = self.outputtypehandler
else:
conn_impl = self._get_conn_impl()
type_handler = conn_impl.outputtypehandler
if type_handler is not None:
uses_fetch_info[0] = \
(inspect.isfunction(type_handler) and \
type_handler.__code__.co_argcount == 2) or \
(inspect.ismethod(type_handler) and \
type_handler.__code__.co_argcount == 3)
return type_handler
cdef int _init_fetch_vars(self, uint32_t num_columns) except -1:
"""
Initializes the fetch variable lists in preparation for creating the
fetch variables used in fetching rows from the database.
"""
self.fetch_info_impls = [None] * num_columns
self.fetch_vars = [None] * num_columns
self.fetch_var_impls = [None] * num_columns

View File

@ -1,5 +1,5 @@
#------------------------------------------------------------------------------
# Copyright (c) 2022, Oracle and/or its affiliates.
# Copyright (c) 2022, 2023, Oracle and/or its affiliates.
#
# 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
@ -200,9 +200,10 @@ def init_base_impl(package):
is to avoid circular imports and eliminate the need for global lookups.
"""
global PY_TYPE_CURSOR, PY_TYPE_DB_OBJECT, PY_TYPE_DB_OBJECT_TYPE
global PY_TYPE_LOB, PY_TYPE_VAR
global PY_TYPE_LOB, PY_TYPE_VAR, PY_TYPE_FETCHINFO
PY_TYPE_CURSOR = package.Cursor
PY_TYPE_DB_OBJECT = package.DbObject
PY_TYPE_DB_OBJECT_TYPE = package.DbObjectType
PY_TYPE_FETCHINFO = package.FetchInfo
PY_TYPE_LOB = package.LOB
PY_TYPE_VAR = package.Var

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
# (UPL) 1.0 as shown at https://oss.oracle.com/licenses/upl and Apache License
@ -259,43 +259,6 @@ cdef class BaseVarImpl:
if size > self.size:
self.size = size
def get_description(self):
"""
Return a 7-tuple containing information about the variable: (name,
type, display_size, internal_size, precision, scale, null_ok).
"""
cdef:
uint32_t display_size = 0, size_in_bytes = 0
FetchInfo fetch_info = self._fetch_info
DbType dbtype = fetch_info._dbtype
object precision, scale
if fetch_info._size > 0:
display_size = fetch_info._size
size_in_bytes = fetch_info._buffer_size
elif dbtype is DB_TYPE_DATE \
or dbtype is DB_TYPE_TIMESTAMP \
or dbtype is DB_TYPE_TIMESTAMP_LTZ \
or dbtype is DB_TYPE_TIMESTAMP_TZ:
display_size = 23
elif dbtype is DB_TYPE_BINARY_FLOAT \
or dbtype is DB_TYPE_BINARY_DOUBLE \
or dbtype is DB_TYPE_BINARY_INTEGER \
or dbtype is DB_TYPE_NUMBER:
if fetch_info._precision:
display_size = fetch_info._precision + 1
if fetch_info._scale > 0:
display_size += fetch_info._scale + 1
else:
display_size = 127
if fetch_info._precision or fetch_info._scale:
precision = fetch_info._precision
scale = fetch_info._scale
else:
scale = precision = None
return (fetch_info._name, dbtype, display_size or None,
size_in_bytes or None, precision, scale,
fetch_info._nulls_allowed)
def get_all_values(self):
"""
Internal method for returning an array of all of the values stored in

View File

@ -69,7 +69,7 @@ cdef class ThickCursorImpl(BaseCursorImpl):
return var_impl
cdef int _define_var(self, object conn, object cursor, object type_handler,
ssize_t pos) except -1:
bint uses_fetch_info, ssize_t pos) except -1:
"""
Internal method that creates the variable using the query info (unless
an output type handler has been specified) and then performs the define
@ -78,37 +78,38 @@ cdef class ThickCursorImpl(BaseCursorImpl):
cdef:
ThickDbObjectTypeImpl typ_impl
dpiDataTypeInfo *type_info
FetchInfoImpl fetch_info
ThickConnImpl conn_impl
dpiQueryInfo query_info
ThickVarImpl var_impl
FetchInfo fetch_info
# build FetchInfo based on query info provided by ODPI-C
# build FetchInfoImpl based on query info provided by ODPI-C
if dpiStmt_getQueryInfo(self._handle, pos + 1, &query_info) < 0:
_raise_from_odpi()
type_info = &query_info.typeInfo
fetch_info = FetchInfo()
fetch_info._dbtype = DbType._from_num(type_info.oracleTypeNum)
if fetch_info._dbtype.num == DPI_ORACLE_TYPE_INTERVAL_YM:
fetch_info = FetchInfoImpl()
fetch_info.dbtype = DbType._from_num(type_info.oracleTypeNum)
if fetch_info.dbtype.num == DPI_ORACLE_TYPE_INTERVAL_YM:
errors._raise_err(errors.ERR_DB_TYPE_NOT_SUPPORTED,
name=fetch_info._dbtype.name)
name=fetch_info.dbtype.name)
if type_info.sizeInChars > 0:
fetch_info._size = type_info.sizeInChars
fetch_info.size = type_info.sizeInChars
else:
fetch_info._size = type_info.clientSizeInBytes
fetch_info._buffer_size = type_info.clientSizeInBytes
fetch_info._name = query_info.name[:query_info.nameLength].decode()
fetch_info._scale = type_info.scale + type_info.fsPrecision
fetch_info._precision = type_info.precision
fetch_info._nulls_allowed = query_info.nullOk
fetch_info._is_json_col = type_info.isJson
fetch_info.size = type_info.clientSizeInBytes
fetch_info.buffer_size = type_info.clientSizeInBytes
fetch_info.name = query_info.name[:query_info.nameLength].decode()
fetch_info.scale = type_info.scale + type_info.fsPrecision
fetch_info.precision = type_info.precision
fetch_info.nulls_allowed = query_info.nullOk
fetch_info.is_json = type_info.isJson
if type_info.objectType != NULL:
typ_impl = ThickDbObjectTypeImpl._from_handle(self._conn_impl,
type_info.objectType)
fetch_info._objtype = typ_impl
fetch_info.objtype = typ_impl
# create variable and call define in ODPI-C
self._create_fetch_var(conn, cursor, type_handler, pos, fetch_info)
self._create_fetch_var(conn, cursor, type_handler, uses_fetch_info,
pos, fetch_info)
var_impl = self.fetch_var_impls[pos]
var_impl._create_handle()
if dpiStmt_define(self._handle, pos + 1, var_impl._handle) < 0:
@ -167,6 +168,7 @@ cdef class ThickCursorImpl(BaseCursorImpl):
ThickCursorImpl cursor_impl = <ThickCursorImpl> cursor._impl
object var, type_handler, conn
ThickVarImpl var_impl
bint uses_fetch_info
ssize_t i
# initialize fetching variables; these are used to reduce the number of
@ -184,10 +186,10 @@ cdef class ThickCursorImpl(BaseCursorImpl):
# populate list to contain fetch variables that are created
self._fetch_array_size = self.arraysize
self._init_fetch_vars(num_query_cols)
type_handler = self._get_output_type_handler()
type_handler = self._get_output_type_handler(&uses_fetch_info)
conn = cursor.connection
for i in range(num_query_cols):
self._define_var(conn, cursor, type_handler, i)
self._define_var(conn, cursor, type_handler, uses_fetch_info, i)
def _set_oci_attr(self, uint32_t attr_num, uint32_t attr_type,
object value):

View File

@ -1,5 +1,5 @@
#------------------------------------------------------------------------------
# Copyright (c) 2021, 2022, Oracle and/or its affiliates.
# Copyright (c) 2021, 2023, Oracle and/or its affiliates.
#
# 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
@ -119,12 +119,12 @@ cdef object _tstamp_to_date(object fetch_value):
return fetch_value.replace(microsecond=0)
cdef int conversion_helper(ThinVarImpl output_var,
FetchInfo fetch_info) except -1:
FetchInfoImpl fetch_info) except -1:
cdef:
uint8_t fetch_ora_type_num, output_ora_type_num, csfrm
object key, value
fetch_ora_type_num = fetch_info._dbtype._ora_type_num
fetch_ora_type_num = fetch_info.dbtype._ora_type_num
output_ora_type_num = output_var.dbtype._ora_type_num
key = (fetch_ora_type_num, output_ora_type_num)
@ -135,11 +135,11 @@ cdef int conversion_helper(ThinVarImpl output_var,
output_var._preferred_num_type = value
else:
csfrm = output_var.dbtype._csfrm
fetch_info._dbtype = DbType._from_ora_type_and_csfrm(value,
csfrm)
fetch_info.dbtype = DbType._from_ora_type_and_csfrm(value,
csfrm)
else:
output_var._conv_func = value
except:
errors._raise_err(errors.ERR_INCONSISTENT_DATATYPES,
input_type=fetch_info._dbtype.name,
input_type=fetch_info.dbtype.name,
output_type=output_var.dbtype.name)

View File

@ -70,8 +70,9 @@ cdef class ThinCursorImpl(BaseCursorImpl):
@cython.boundscheck(False)
@cython.wraparound(False)
cdef int _create_fetch_var(self, object conn, object cursor,
object type_handler, ssize_t pos,
FetchInfo fetch_info) except -1:
object type_handler, bint uses_fetch_info,
ssize_t pos,
FetchInfoImpl fetch_info) except -1:
"""
Internal method that creates a fetch variable. A check is made after
the variable is created to determine if a conversion is required and
@ -80,10 +81,10 @@ cdef class ThinCursorImpl(BaseCursorImpl):
cdef:
ThinDbObjectTypeImpl typ_impl
ThinVarImpl var_impl
BaseCursorImpl._create_fetch_var(self, conn, cursor, type_handler, pos,
fetch_info)
BaseCursorImpl._create_fetch_var(self, conn, cursor, type_handler,
uses_fetch_info, pos, fetch_info)
var_impl = self.fetch_var_impls[pos]
if var_impl.dbtype._ora_type_num != fetch_info._dbtype._ora_type_num:
if var_impl.dbtype._ora_type_num != fetch_info.dbtype._ora_type_num:
conversion_helper(var_impl, fetch_info)
elif var_impl.objtype is not None:
typ_impl = var_impl.objtype
@ -202,6 +203,7 @@ cdef class ThinCursorImpl(BaseCursorImpl):
self._statement = None
self._statement = self._conn_impl._get_statement(sql.strip(),
cache_statement)
self.fetch_info_impls = self._statement._fetch_info_impls
self.fetch_vars = self._statement._fetch_vars
self.fetch_var_impls = self._statement._fetch_var_impls
self._num_columns = self._statement._num_columns

View File

@ -318,7 +318,7 @@ cdef class MessageWithData(Message):
cdef int _adjust_fetch_info(self,
ThinVarImpl prev_var_impl,
FetchInfo fetch_info) except -1:
FetchInfoImpl fetch_info) except -1:
"""
When a query is re-executed but the data type of a column has changed
the server returns the type information of the new type. However, if
@ -329,22 +329,22 @@ cdef class MessageWithData(Message):
Detect this situation and adjust the fetch type appropriately.
"""
cdef:
FetchInfo prev_fetch_info = prev_var_impl._fetch_info
FetchInfoImpl prev_fetch_info = prev_var_impl._fetch_info
uint8_t csfrm = prev_var_impl.dbtype._csfrm
uint8_t type_num
if fetch_info._dbtype._ora_type_num == TNS_DATA_TYPE_CLOB \
and prev_fetch_info._dbtype._ora_type_num in \
if fetch_info.dbtype._ora_type_num == TNS_DATA_TYPE_CLOB \
and prev_fetch_info.dbtype._ora_type_num in \
(TNS_DATA_TYPE_CHAR, TNS_DATA_TYPE_VARCHAR,
TNS_DATA_TYPE_LONG):
type_num = TNS_DATA_TYPE_LONG
fetch_info._dbtype = DbType._from_ora_type_and_csfrm(type_num,
csfrm)
elif fetch_info._dbtype._ora_type_num == TNS_DATA_TYPE_BLOB \
and prev_fetch_info._dbtype._ora_type_num in \
fetch_info.dbtype = DbType._from_ora_type_and_csfrm(type_num,
csfrm)
elif fetch_info.dbtype._ora_type_num == TNS_DATA_TYPE_BLOB \
and prev_fetch_info.dbtype._ora_type_num in \
(TNS_DATA_TYPE_RAW, TNS_DATA_TYPE_LONG_RAW):
type_num = TNS_DATA_TYPE_LONG_RAW
fetch_info._dbtype = DbType._from_ora_type_and_csfrm(type_num,
csfrm)
fetch_info.dbtype = DbType._from_ora_type_and_csfrm(type_num,
csfrm)
cdef object _create_cursor_from_describe(self, ReadBuffer buf,
object cursor=None):
@ -432,6 +432,7 @@ cdef class MessageWithData(Message):
Statement statement = cursor_impl._statement
object type_handler, conn
ThinVarImpl var_impl
bint uses_fetch_info
ssize_t i, num_vals
# set values to indicate the start of a new fetch operation
@ -449,11 +450,12 @@ cdef class MessageWithData(Message):
# the one that was used during the last fetch, rebuild the fetch
# variables in order to take the new type handler into account
conn = self.cursor.connection
type_handler = cursor_impl._get_output_type_handler()
type_handler = cursor_impl._get_output_type_handler(&uses_fetch_info)
if type_handler is not statement._last_output_type_handler:
for i, var_impl in enumerate(cursor_impl.fetch_var_impls):
cursor_impl._create_fetch_var(conn, self.cursor, type_handler,
i, var_impl._fetch_info)
uses_fetch_info, i,
var_impl._fetch_info)
statement._last_output_type_handler = type_handler
# the list of output variables is equivalent to the fetch variables
@ -518,15 +520,15 @@ cdef class MessageWithData(Message):
ThinCursorImpl cursor_impl
object column_value = None
ThinDbObjectImpl obj_impl
FetchInfoImpl fetch_info
int32_t actual_num_bytes
uint32_t buffer_size
FetchInfo fetch_info
Rowid rowid
fetch_info = var_impl._fetch_info
if fetch_info is not None:
ora_type_num = fetch_info._dbtype._ora_type_num
csfrm = fetch_info._dbtype._csfrm
buffer_size = fetch_info._buffer_size
ora_type_num = fetch_info.dbtype._ora_type_num
csfrm = fetch_info.dbtype._csfrm
buffer_size = fetch_info.buffer_size
else:
ora_type_num = var_impl.dbtype._ora_type_num
csfrm = var_impl.dbtype._csfrm
@ -628,33 +630,33 @@ cdef class MessageWithData(Message):
column_value = var_impl._conv_func(column_value)
return column_value
cdef FetchInfo _process_column_info(self, ReadBuffer buf,
ThinCursorImpl cursor_impl):
cdef FetchInfoImpl _process_column_info(self, ReadBuffer buf,
ThinCursorImpl cursor_impl):
cdef:
ThinDbObjectTypeImpl typ_impl
uint32_t num_bytes, uds_flags
uint8_t data_type, csfrm
FetchInfoImpl fetch_info
int8_t precision, scale
uint8_t nulls_allowed
FetchInfo fetch_info
str schema, name
int cache_num
bytes oid
buf.read_ub1(&data_type)
fetch_info = FetchInfo()
fetch_info = FetchInfoImpl()
buf.skip_ub1() # flags
buf.read_sb1(&precision)
fetch_info._precision = precision
fetch_info.precision = precision
if data_type == TNS_DATA_TYPE_NUMBER \
or data_type == TNS_DATA_TYPE_INTERVAL_DS \
or data_type == TNS_DATA_TYPE_TIMESTAMP \
or data_type == TNS_DATA_TYPE_TIMESTAMP_LTZ \
or data_type == TNS_DATA_TYPE_TIMESTAMP_TZ:
buf.read_sb2(&fetch_info._scale)
buf.read_sb2(&fetch_info.scale)
else:
buf.read_sb1(&scale)
fetch_info._scale = scale
buf.read_ub4(&fetch_info._buffer_size)
fetch_info.scale = scale
buf.read_ub4(&fetch_info.buffer_size)
buf.skip_ub4() # max number of array elements
buf.skip_ub4() # cont flags
buf.read_ub4(&num_bytes) # OID
@ -663,18 +665,18 @@ cdef class MessageWithData(Message):
buf.skip_ub2() # version
buf.skip_ub2() # character set id
buf.read_ub1(&csfrm) # character set form
fetch_info._dbtype = DbType._from_ora_type_and_csfrm(data_type, csfrm)
buf.read_ub4(&fetch_info._size)
fetch_info.dbtype = DbType._from_ora_type_and_csfrm(data_type, csfrm)
buf.read_ub4(&fetch_info.size)
if data_type == TNS_DATA_TYPE_RAW:
fetch_info._size = fetch_info._buffer_size
fetch_info.size = fetch_info.buffer_size
if buf._caps.ttc_field_version >= TNS_CCAP_FIELD_VERSION_12_2:
buf.skip_ub4() # oaccolid
buf.read_ub1(&nulls_allowed)
fetch_info._nulls_allowed = nulls_allowed
fetch_info.nulls_allowed = nulls_allowed
buf.skip_ub1() # v7 length of name
buf.read_ub4(&num_bytes)
if num_bytes > 0:
fetch_info._name = buf.read_str(TNS_CS_IMPLICIT)
fetch_info.name = buf.read_str(TNS_CS_IMPLICIT)
buf.read_ub4(&num_bytes)
if num_bytes > 0:
schema = buf.read_str(TNS_CS_IMPLICIT)
@ -683,14 +685,14 @@ cdef class MessageWithData(Message):
name = buf.read_str(TNS_CS_IMPLICIT)
buf.skip_ub2() # column position
buf.read_ub4(&uds_flags)
fetch_info._is_json_col = uds_flags & TNS_UDS_FLAGS_IS_JSON
fetch_info.is_json = uds_flags & TNS_UDS_FLAGS_IS_JSON
if data_type == TNS_DATA_TYPE_INT_NAMED:
if self.type_cache is None:
cache_num = self.conn_impl._dbobject_type_cache_num
self.type_cache = get_dbobject_type_cache(cache_num)
typ_impl = self.type_cache.get_type_for_info(oid, schema, None,
name)
fetch_info._objtype = typ_impl
fetch_info.objtype = typ_impl
return fetch_info
cdef int _process_describe_info(self, ReadBuffer buf,
@ -700,8 +702,9 @@ cdef class MessageWithData(Message):
Statement stmt = cursor_impl._statement
list prev_fetch_var_impls
object type_handler, conn
FetchInfoImpl fetch_info
uint32_t num_bytes, i
FetchInfo fetch_info
bint uses_fetch_info
str message
buf.skip_ub4() # max row size
buf.read_ub4(&cursor_impl._num_columns)
@ -709,7 +712,7 @@ cdef class MessageWithData(Message):
cursor_impl._init_fetch_vars(cursor_impl._num_columns)
if cursor_impl._num_columns > 0:
buf.skip_ub1()
type_handler = cursor_impl._get_output_type_handler()
type_handler = cursor_impl._get_output_type_handler(&uses_fetch_info)
conn = self.cursor.connection
for i in range(cursor_impl._num_columns):
fetch_info = self._process_column_info(buf, cursor_impl)
@ -717,13 +720,13 @@ cdef class MessageWithData(Message):
and i < len(prev_fetch_var_impls):
self._adjust_fetch_info(prev_fetch_var_impls[i], fetch_info)
if not stmt._no_prefetch and \
fetch_info._dbtype._ora_type_num in (TNS_DATA_TYPE_BLOB,
TNS_DATA_TYPE_CLOB,
TNS_DATA_TYPE_JSON):
fetch_info.dbtype._ora_type_num in (TNS_DATA_TYPE_BLOB,
TNS_DATA_TYPE_CLOB,
TNS_DATA_TYPE_JSON):
stmt._requires_define = True
stmt._no_prefetch = True
cursor_impl._create_fetch_var(conn, self.cursor, type_handler, i,
fetch_info)
cursor_impl._create_fetch_var(conn, self.cursor, type_handler,
uses_fetch_info, i, fetch_info)
buf.read_ub4(&num_bytes)
if num_bytes > 0:
buf.skip_raw_bytes_chunked() # current date
@ -734,6 +737,7 @@ cdef class MessageWithData(Message):
buf.read_ub4(&num_bytes)
if num_bytes > 0:
buf.skip_raw_bytes_chunked() # dcbqcky
stmt._fetch_info_impls = cursor_impl.fetch_info_impls
stmt._fetch_vars = cursor_impl.fetch_vars
stmt._fetch_var_impls = cursor_impl.fetch_var_impls
stmt._num_columns = cursor_impl._num_columns

View File

@ -88,6 +88,7 @@ cdef class Statement:
bint _is_ddl
bint _is_returning
list _bind_info_list
list _fetch_info_impls
list _fetch_vars
list _fetch_var_impls
object _bind_info_dict

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
# (UPL) 1.0 as shown at https://oss.oracle.com/licenses/upl and Apache License
@ -52,8 +52,8 @@ from .base_impl cimport BaseDbObjectImpl, BaseLobImpl, BasePoolImpl
from .base_impl cimport BaseSodaDbImpl, BaseSodaCollImpl, BaseSodaDocImpl
from .base_impl cimport BaseSodaDocCursorImpl, BaseQueueImpl
from .base_impl cimport BaseDeqOptionsImpl, BaseEnqOptionsImpl
from .base_impl cimport BaseMsgPropsImpl, BaseSubscrImpl, BindVar, FetchInfo
from .base_impl cimport ConnectParamsImpl, PoolParamsImpl
from .base_impl cimport BaseMsgPropsImpl, BaseSubscrImpl, BindVar
from .base_impl cimport ConnectParamsImpl, PoolParamsImpl, FetchInfoImpl
from .base_impl cimport NUM_TYPE_FLOAT, NUM_TYPE_INT, NUM_TYPE_DECIMAL
from libc.string cimport memchr, memset

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
# (UPL) 1.0 as shown at https://oss.oracle.com/licenses/upl and Apache License
@ -77,7 +77,7 @@ from .defaults import defaults
from .base_impl cimport get_exception_class, NUM_TYPE_FLOAT
from .base_impl cimport NUM_TYPE_INT, NUM_TYPE_DECIMAL, NUM_TYPE_STR
from .base_impl cimport BaseConnImpl, BaseCursorImpl, BaseVarImpl, DbType
from .base_impl cimport BaseLobImpl, BasePoolImpl, FetchInfo
from .base_impl cimport BaseLobImpl, BasePoolImpl, FetchInfoImpl
from .base_impl cimport Address, AddressList, Description, DescriptionList
from .base_impl cimport ConnectParamsImpl, PoolParamsImpl, BaseDbObjectAttrImpl
from .base_impl cimport BaseDbObjectImpl, BaseDbObjectTypeImpl

View File

@ -192,7 +192,7 @@ class TestCase(test_env.BaseTestCase):
def test_1309_output_type_handler_with_ref_cursor(self):
"1309 - test using an output type handler with a REF cursor"
def type_handler(cursor, name, default_type, size, precision, scale):
def type_handler(cursor, metadata):
return cursor.var(str, arraysize=cursor.arraysize)
self.connection.outputtypehandler = type_handler
var = self.cursor.var(oracledb.DB_TYPE_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
# (UPL) 1.0 as shown at https://oss.oracle.com/licenses/upl and Apache License
@ -33,19 +33,16 @@ import test_env
class TestCase(test_env.BaseTestCase):
def output_type_handler_binary_int(self, cursor, name, default_type, size,
precision, scale):
def output_type_handler_binary_int(self, cursor, metadata):
return cursor.var(oracledb.DB_TYPE_BINARY_INTEGER,
arraysize=cursor.arraysize)
def output_type_handler_decimal(self, cursor, name, default_type, size,
precision, scale):
if default_type == oracledb.NUMBER:
def output_type_handler_decimal(self, cursor, metadata):
if metadata.type_code is oracledb.DB_TYPE_NUMBER:
return cursor.var(str, 255, outconverter=decimal.Decimal,
arraysize=cursor.arraysize)
def output_type_handler_str(self, cursor, name, default_type, size,
precision, scale):
def output_type_handler_str(self, cursor, metadata):
return cursor.var(str, 255, arraysize=cursor.arraysize)

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
# (UPL) 1.0 as shown at https://oss.oracle.com/licenses/upl and Apache License
@ -51,9 +51,8 @@ class TestCase(test_env.BaseTestCase):
self.raw_data.append(data_tuple)
self.data_by_key[i] = data_tuple
def __return_strings_as_bytes(self, cursor, name, default_type, size,
precision, scale):
if default_type == oracledb.DB_TYPE_VARCHAR:
def __return_strings_as_bytes(self, cursor, metadata):
if metadata.type_code is oracledb.DB_TYPE_VARCHAR:
return cursor.var(str, arraysize=cursor.arraysize,
bypass_decode=True)

View File

@ -37,7 +37,7 @@ class TestCase(test_env.BaseTestCase):
def __test_type_handler(self, input_type, output_type, in_value,
expected_out_value):
def type_handler(cursor, name, default_type, size, precision, scale):
def type_handler(cursor, metadata):
return cursor.var(output_type, arraysize=cursor.arraysize)
self.cursor.outputtypehandler = type_handler
var = self.cursor.var(input_type)
@ -49,8 +49,8 @@ class TestCase(test_env.BaseTestCase):
def __test_type_handler_lob(self, lob_type, output_type):
db_type = getattr(oracledb, lob_type)
def type_handler(cursor, name, default_type, size, precision, scale):
if default_type == db_type:
def type_handler(cursor, metadata):
if metadata.type_code is db_type:
return cursor.var(output_type, arraysize=cursor.arraysize)
self.cursor.outputtypehandler = type_handler
in_value = f"Some {lob_type} data"
@ -479,7 +479,7 @@ class TestCase(test_env.BaseTestCase):
def test_3671_incorrect_arraysize(self):
"3671 - execute raises an error if an incorrect arraysize is used"
def type_handler(cursor, name, default_type, size, precision, scale):
def type_handler(cursor, metadata):
return cursor.var(str)
cursor = self.connection.cursor()
cursor.arraysize = 100
@ -489,8 +489,7 @@ class TestCase(test_env.BaseTestCase):
def test_3672_incorrect_outputtypehandler_return_type(self):
"3672 - execute raises an error if a var is not returned"
def type_handler(cursor, name, default_type, size,
precision, scale):
def type_handler(cursor, metadata):
return "incorrect_return"
cursor = self.connection.cursor()
cursor.outputtypehandler = type_handler
@ -506,8 +505,7 @@ class TestCase(test_env.BaseTestCase):
def test_3674_cursor_description_unchanged(self):
"3674 - use of output type handler does not affect description"
def type_handler(cursor, name, default_type, size,
precision, scale):
def type_handler(cursor, metadata):
return cursor.var(str, arraysize=cursor.arraysize)
with self.connection.cursor() as cursor:
cursor.execute("select user from dual")
@ -517,5 +515,14 @@ class TestCase(test_env.BaseTestCase):
cursor.execute("select user from dual")
self.assertEqual(cursor.description, desc_before)
def test_3675_old_signature(self):
"3675 - use the old signature for an output type handler"
def type_handler(cursor, name, default_type, size, precision, scale):
return cursor.var(str, arraysize=cursor.arraysize)
with self.connection.cursor() as cursor:
cursor.outputtypehandler = type_handler
cursor.execute("select 1 from dual")
self.assertEqual(cursor.fetchall(), [('1',)])
if __name__ == "__main__":
test_env.run_test_cases()

View File

@ -1,5 +1,5 @@
#------------------------------------------------------------------------------
# Copyright (c) 2021, 2022, Oracle and/or its affiliates.
# Copyright (c) 2021, 2023, Oracle and/or its affiliates.
#
# 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
@ -68,10 +68,9 @@ class TestCase(test_env.BaseTestCase):
return cursor.var(oracledb.STRING, arraysize=num_elements,
inconverter=self.building_in_converter)
def output_type_handler(self, cursor, name, default_type, size, precision,
scale):
if default_type == oracledb.STRING:
return cursor.var(default_type, arraysize=cursor.arraysize,
def output_type_handler(self, cursor, metadata):
if metadata.type_code is oracledb.DB_TYPE_VARCHAR:
return cursor.var(metadata.type_code, arraysize=cursor.arraysize,
outconverter=Building.from_json)
def test_3800(self):
@ -173,9 +172,8 @@ class TestCase(test_env.BaseTestCase):
self.connection.commit()
def converter(value):
return "CONVERTED"
def output_type_handler(cursor, name, default_type, size, precision,
scale):
if default_type is oracledb.DB_TYPE_VARCHAR:
def output_type_handler(cursor, metadata):
if metadata.type_code is oracledb.DB_TYPE_VARCHAR:
return cursor.var(str, outconverter=converter,
arraysize=cursor.arraysize)
self.cursor.outputtypehandler = output_type_handler
@ -212,16 +210,15 @@ class TestCase(test_env.BaseTestCase):
# take advantage of direct binding
self.cursor.setinputsizes(None, oracledb.DB_TYPE_JSON)
self.cursor.executemany(insert_sql, data_to_insert)
def output_type_handler(cursor, name, default_type, size, precision,
scale):
def output_type_handler(cursor, metadata):
# fetch 21c JSON datatype when using python-oracledb thin mode
if default_type == oracledb.DB_TYPE_JSON:
if metadata.type_code is oracledb.DB_TYPE_JSON:
return cursor.var(str, arraysize=cursor.arraysize,
outconverter=json.loads)
# if using Oracle Client version < 21, then database returns BLOB
# data type instead of JSON data type
elif default_type == oracledb.DB_TYPE_BLOB:
return cursor.var(default_type, arraysize=cursor.arraysize,
elif metadata.type_code is oracledb.DB_TYPE_BLOB:
return cursor.var(metadata.type, arraysize=cursor.arraysize,
outconverter=lambda v: json.loads(v.read()))
if json_as_string:
self.cursor.outputtypehandler = output_type_handler

View File

@ -374,8 +374,8 @@ class TestCase(test_env.BaseTestCase):
def test_3929_output_type_handler_with_prefetch_gt_arraysize(self):
"3929 - test an output type handler with prefetch > arraysize"
def type_handler(cursor, name, default_type, size, precision, scale):
return cursor.var(default_type, arraysize=cursor.arraysize)
def type_handler(cursor, metadata):
return cursor.var(metadata.type_code, arraysize=cursor.arraysize)
self.cursor.arraysize = 2
self.cursor.prefetchrows = 3
self.cursor.outputtypehandler = type_handler

View File

@ -588,8 +588,8 @@ class TestCase(test_env.BaseTestCase):
def test_4348_reexecute_query_with_blob_as_bytes(self):
"4348 - test re-executing a query with blob as bytes"
def type_handler(cursor, name, default_type, size, precision, scale):
if default_type == oracledb.DB_TYPE_BLOB:
def type_handler(cursor, metadata):
if metadata.type_code is oracledb.DB_TYPE_BLOB:
return cursor.var(bytes, arraysize=cursor.arraysize)
self.connection.outputtypehandler = type_handler
blob_data = b"An arbitrary set of blob data for test case 4348"
@ -700,8 +700,8 @@ class TestCase(test_env.BaseTestCase):
def out_converter(value):
self.assertIs(type(value), str)
return int(value)
def type_handler(cursor, name, default_type, size, precision, scale):
if name == "COL_3":
def type_handler(cursor, metadata):
if metadata.name == "COL_3":
return cursor.var(str, arraysize=cursor.arraysize,
outconverter=out_converter)
self.cursor.outputtypehandler = type_handler