Skip to content

Commit c22de66

Browse files
Process %ROWTYPE types directly in the driver to workaround database bug
(#304).
1 parent 9f3ef42 commit c22de66

File tree

8 files changed

+216
-41
lines changed

8 files changed

+216
-41
lines changed

doc/src/release_notes.rst

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,9 @@ oracledb 2.1.1 (TBD)
1313
Thin Mode Changes
1414
+++++++++++++++++
1515

16+
#) Fix bug when calling `~Connection.gettype()` with an object type name
17+
containing ``%ROWTYPE``
18+
(`issue 304 <https://github.com/oracle/python-oracledb/issues/304>`__).
1619
#) Restored error message raised when attempting to connect to an 11g
1720
database.
1821

src/oracledb/impl/base/types.pyx

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -178,12 +178,13 @@ DB_TYPE_TIMESTAMP = DbType(DB_TYPE_NUM_TIMESTAMP, "DB_TYPE_TIMESTAMP",
178178
buffer_size_factor=11)
179179
DB_TYPE_TIMESTAMP_LTZ = DbType(DB_TYPE_NUM_TIMESTAMP_LTZ,
180180
"DB_TYPE_TIMESTAMP_LTZ",
181-
"TIMESTAMP WITH LOCAL TZ",
181+
"TIMESTAMP WITH LOCAL TIME ZONE",
182182
NATIVE_TYPE_NUM_TIMESTAMP, 231,
183183
buffer_size_factor=11)
184184
DB_TYPE_TIMESTAMP_TZ = DbType(DB_TYPE_NUM_TIMESTAMP_TZ, "DB_TYPE_TIMESTAMP_TZ",
185-
"TIMESTAMP WITH TZ", NATIVE_TYPE_NUM_TIMESTAMP,
186-
181, buffer_size_factor=13)
185+
"TIMESTAMP WITH TIME ZONE",
186+
NATIVE_TYPE_NUM_TIMESTAMP, 181,
187+
buffer_size_factor=13)
187188
DB_TYPE_UNKNOWN = DbType(DB_TYPE_NUM_UNKNOWN, "DB_TYPE_UNKNOWN", "UNKNOWN")
188189
DB_TYPE_UROWID = DbType(DB_TYPE_NUM_UROWID, "DB_TYPE_UROWID", "UROWID",
189190
NATIVE_TYPE_NUM_BYTES, 208)
@@ -205,6 +206,8 @@ db_type_by_ora_name["PL/SQL BINARY INTEGER"] = DB_TYPE_BINARY_INTEGER
205206
db_type_by_ora_name["PL/SQL PLS INTEGER"] = DB_TYPE_BINARY_INTEGER
206207
db_type_by_ora_name["REAL"] = DB_TYPE_NUMBER
207208
db_type_by_ora_name["SMALLINT"] = DB_TYPE_NUMBER
209+
db_type_by_ora_name["TIMESTAMP WITH TZ"] = DB_TYPE_TIMESTAMP_TZ
210+
db_type_by_ora_name["TIMESTAMP WITH LOCAL TZ"] = DB_TYPE_TIMESTAMP_LTZ
208211

209212
# DB API types
210213
BINARY = ApiType("BINARY", DB_TYPE_RAW, DB_TYPE_LONG_RAW)

src/oracledb/impl/thin/dbobject.pyx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -575,6 +575,7 @@ cdef class ThinDbObjectTypeImpl(BaseDbObjectTypeImpl):
575575
cdef:
576576
uint8_t collection_type, collection_flags, version
577577
uint32_t max_num_elements
578+
bint is_row_type
578579
bint is_xml_type
579580
bytes oid
580581

src/oracledb/impl/thin/dbobject_cache.pyx

Lines changed: 115 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,24 @@ cdef str DBO_CACHE_SQL_GET_METADATA_FOR_NAME = """
6767
end if;
6868
end;"""
6969

70+
cdef str DBO_CACHE_SQL_GET_COLUMNS = """
71+
select
72+
column_name,
73+
data_type,
74+
data_type_owner,
75+
case
76+
when data_type in
77+
('CHAR', 'NCHAR', 'VARCHAR2', 'NVARCHAR2', 'RAW')
78+
then data_length
79+
else 0
80+
end,
81+
nvl(data_precision, 0),
82+
nvl(data_scale, 0)
83+
from all_tab_columns
84+
where owner = :owner
85+
and table_name = substr(:name, 1, length(:name) - 8)
86+
order by column_id"""
87+
7088
cdef str DBO_CACHE_SQL_GET_ELEM_TYPE_WITH_PACKAGE = """
7189
select elem_type_name
7290
from all_plsql_coll_types
@@ -113,17 +131,17 @@ cdef class ThinDbObjectTypeSuperCache:
113131
cdef class BaseThinDbObjectTypeCache:
114132

115133
cdef:
134+
object meta_cursor, columns_cursor, attrs_ref_cursor_var, version_var
116135
object return_value_var, full_name_var, oid_var, tds_var
117-
object meta_cursor, attrs_ref_cursor_var, version_var
118136
object schema_var, package_name_var, name_var
119137
BaseThinConnImpl conn_impl
120138
dict types_by_oid
121139
dict types_by_name
122140
list partial_types
123141

124-
cdef int _clear_meta_cursor(self) except -1:
142+
cdef int _clear_cursors(self) except -1:
125143
"""
126-
Clears the cursor used for searching metadata. This is needed when
144+
Clears the cursors used for searching metadata. This is needed when
127145
returning a connection to the pool since user-level objects are
128146
retained.
129147
"""
@@ -139,6 +157,9 @@ cdef class BaseThinDbObjectTypeCache:
139157
self.schema_var = None
140158
self.package_name_var = None
141159
self.name_var = None
160+
if self.columns_cursor is not None:
161+
self.columns_cursor.close()
162+
self.columns_cursor = None
142163

143164
cdef str _get_full_name(self, ThinDbObjectTypeImpl typ_impl):
144165
"""
@@ -163,6 +184,17 @@ cdef class BaseThinDbObjectTypeCache:
163184
self.partial_types = []
164185
self.conn_impl = conn_impl
165186

187+
cdef int _init_columns_cursor(self, object conn) except -1:
188+
"""
189+
Initializes the cursor that fetches the columns for a table or view.
190+
The input values come from the meta cursor that has been initialized
191+
and executed earlier.
192+
"""
193+
cursor = conn.cursor()
194+
cursor.setinputsizes(owner=self.schema_var, name=self.name_var)
195+
cursor.prepare(DBO_CACHE_SQL_GET_COLUMNS)
196+
self.columns_cursor = cursor
197+
166198
cdef int _init_meta_cursor(self, object conn) except -1:
167199
"""
168200
Initializes the cursor that fetches the type metadata.
@@ -338,8 +370,9 @@ cdef class BaseThinDbObjectTypeCache:
338370
Populate the type information given the name of the type.
339371
"""
340372
cdef:
373+
ssize_t start_pos, end_pos, name_length
341374
ThinDbObjectAttrImpl attr_impl
342-
ssize_t pos, name_length
375+
str data_type
343376
typ_impl.version = self.version_var.getvalue()
344377
if typ_impl.oid is None:
345378
typ_impl.oid = self.oid_var.getvalue()
@@ -352,25 +385,52 @@ cdef class BaseThinDbObjectTypeCache:
352385
(typ_impl.schema == "SYS" and typ_impl.name == "XMLTYPE")
353386
typ_impl.attrs = []
354387
typ_impl.attrs_by_name = {}
355-
for cursor_version, attr_name, attr_num, attr_type_name, \
356-
attr_type_owner, attr_type_package, attr_type_oid, \
357-
attr_instantiable, attr_super_type_owner, \
358-
attr_super_type_name in attrs:
359-
if attr_name is None:
360-
continue
361-
attr_impl = ThinDbObjectAttrImpl.__new__(ThinDbObjectAttrImpl)
362-
attr_impl.name = attr_name
363-
if attr_type_owner is not None:
364-
attr_impl.dbtype = DB_TYPE_OBJECT
365-
attr_impl.objtype = self.get_type_for_info(attr_type_oid,
366-
attr_type_owner,
367-
attr_type_package,
368-
attr_type_name)
369-
else:
370-
attr_impl.dbtype = DbType._from_ora_name(attr_type_name)
371-
typ_impl.attrs.append(attr_impl)
372-
typ_impl.attrs_by_name[attr_name] = attr_impl
373-
return self._parse_tds(typ_impl, self.tds_var.getvalue())
388+
if typ_impl.is_row_type:
389+
for name, data_type, data_type_owner, max_size, precision, \
390+
scale in attrs:
391+
attr_impl = ThinDbObjectAttrImpl.__new__(ThinDbObjectAttrImpl)
392+
attr_impl.name = name
393+
if data_type_owner is not None:
394+
attr_impl.dbtype = DB_TYPE_OBJECT
395+
attr_impl.objtype = self.get_type_for_info(None,
396+
data_type_owner,
397+
None,
398+
data_type)
399+
else:
400+
start_pos = data_type.find("(")
401+
if start_pos > 0:
402+
end_pos = data_type.find(")")
403+
if end_pos > start_pos:
404+
data_type = data_type[:start_pos] + \
405+
data_type[end_pos + 1:]
406+
attr_impl.dbtype = DbType._from_ora_name(data_type)
407+
attr_impl.max_size = max_size
408+
attr_impl.precision = precision
409+
attr_impl.scale = scale
410+
typ_impl.attrs.append(attr_impl)
411+
typ_impl.attrs_by_name[name] = attr_impl
412+
else:
413+
for cursor_version, attr_name, attr_num, attr_type_name, \
414+
attr_type_owner, attr_type_package, attr_type_oid, \
415+
attr_instantiable, attr_super_type_owner, \
416+
attr_super_type_name in attrs:
417+
if attr_name is None:
418+
continue
419+
attr_impl = ThinDbObjectAttrImpl.__new__(ThinDbObjectAttrImpl)
420+
attr_impl.name = attr_name
421+
if attr_type_owner is not None:
422+
attr_impl.dbtype = DB_TYPE_OBJECT
423+
attr_impl.objtype = self.get_type_for_info(
424+
attr_type_oid,
425+
attr_type_owner,
426+
attr_type_package,
427+
attr_type_name
428+
)
429+
else:
430+
attr_impl.dbtype = DbType._from_ora_name(attr_type_name)
431+
typ_impl.attrs.append(attr_impl)
432+
typ_impl.attrs_by_name[attr_name] = attr_impl
433+
return self._parse_tds(typ_impl, self.tds_var.getvalue())
374434

375435
cdef ThinDbObjectTypeImpl get_type_for_info(self, bytes oid, str schema,
376436
str package_name, str name):
@@ -452,7 +512,8 @@ cdef class ThinDbObjectTypeCache(BaseThinDbObjectTypeCache):
452512
typ_impl.element_objtype = self.get_type_for_info(None, schema,
453513
package_name, name)
454514

455-
cdef list _lookup_type(self, object conn, str name):
515+
cdef list _lookup_type(self, object conn, str name,
516+
ThinDbObjectTypeImpl typ_impl):
456517
"""
457518
Lookup the type given its name and return the list of attributes for
458519
further processing. The metadata cursor execution will populate the
@@ -464,24 +525,33 @@ cdef class ThinDbObjectTypeCache(BaseThinDbObjectTypeCache):
464525
self.meta_cursor.execute(None)
465526
if self.return_value_var.getvalue() != 0:
466527
errors._raise_err(errors.ERR_INVALID_OBJECT_TYPE_NAME, name=name)
467-
attrs_rc = self.attrs_ref_cursor_var.getvalue()
468-
return attrs_rc.fetchall()
528+
if name.endswith("%ROWTYPE"):
529+
typ_impl.is_row_type = True
530+
if self.columns_cursor is None:
531+
self._init_columns_cursor(conn)
532+
self.columns_cursor.execute(None)
533+
return self.columns_cursor.fetchall()
534+
else:
535+
attrs_rc = self.attrs_ref_cursor_var.getvalue()
536+
return attrs_rc.fetchall()
469537

470538
cdef ThinDbObjectTypeImpl get_type(self, object conn, str name):
471539
"""
472540
Returns the database object type given its name. The cache is first
473541
searched and if it is not found, the database is searched and the
474542
result stored in the cache.
475543
"""
476-
cdef ThinDbObjectTypeImpl typ_impl
544+
cdef:
545+
ThinDbObjectTypeImpl typ_impl
546+
bint is_rowtype
477547
typ_impl = self.types_by_name.get(name)
478548
if typ_impl is None:
479-
attrs = self._lookup_type(conn, name)
480549
typ_impl = ThinDbObjectTypeImpl.__new__(ThinDbObjectTypeImpl)
481550
typ_impl._conn_impl = self.conn_impl
551+
attrs = self._lookup_type(conn, name, typ_impl)
552+
self._populate_type_info(name, attrs, typ_impl)
482553
self.types_by_oid[typ_impl.oid] = typ_impl
483554
self.types_by_name[name] = typ_impl
484-
self._populate_type_info(name, attrs, typ_impl)
485555
self.populate_partial_types(conn)
486556
return typ_impl
487557

@@ -499,7 +569,7 @@ cdef class ThinDbObjectTypeCache(BaseThinDbObjectTypeCache):
499569
while self.partial_types:
500570
typ_impl = self.partial_types.pop()
501571
full_name = self._get_full_name(typ_impl)
502-
attrs = self._lookup_type(conn, full_name)
572+
attrs = self._lookup_type(conn, full_name, typ_impl)
503573
self._populate_type_info(full_name, attrs, typ_impl)
504574

505575

@@ -549,7 +619,8 @@ cdef class AsyncThinDbObjectTypeCache(BaseThinDbObjectTypeCache):
549619
typ_impl.element_objtype = self.get_type_for_info(None, schema,
550620
package_name, name)
551621

552-
async def _lookup_type(self, object conn, str name):
622+
async def _lookup_type(self, object conn, str name,
623+
ThinDbObjectTypeImpl typ_impl):
553624
"""
554625
Lookup the type given its name and return the list of attributes for
555626
further processing. The metadata cursor execution will populate the
@@ -561,8 +632,15 @@ cdef class AsyncThinDbObjectTypeCache(BaseThinDbObjectTypeCache):
561632
await self.meta_cursor.execute(None)
562633
if self.return_value_var.getvalue() != 0:
563634
errors._raise_err(errors.ERR_INVALID_OBJECT_TYPE_NAME, name=name)
564-
attrs_rc = self.attrs_ref_cursor_var.getvalue()
565-
return await attrs_rc.fetchall()
635+
if name.endswith("%ROWTYPE"):
636+
typ_impl.is_row_type = True
637+
if self.columns_cursor is None:
638+
self._init_columns_cursor(conn)
639+
await self.columns_cursor.execute(None)
640+
return await self.columns_cursor.fetchall()
641+
else:
642+
attrs_rc = self.attrs_ref_cursor_var.getvalue()
643+
return await attrs_rc.fetchall()
566644

567645
async def get_type(self, object conn, str name):
568646
"""
@@ -573,14 +651,14 @@ cdef class AsyncThinDbObjectTypeCache(BaseThinDbObjectTypeCache):
573651
cdef ThinDbObjectTypeImpl typ_impl
574652
typ_impl = self.types_by_name.get(name)
575653
if typ_impl is None:
576-
attrs = await self._lookup_type(conn, name)
577654
typ_impl = ThinDbObjectTypeImpl.__new__(ThinDbObjectTypeImpl)
578655
typ_impl._conn_impl = self.conn_impl
579-
self.types_by_oid[typ_impl.oid] = typ_impl
580-
self.types_by_name[name] = typ_impl
656+
attrs = await self._lookup_type(conn, name, typ_impl)
581657
coroutine = self._populate_type_info(name, attrs, typ_impl)
582658
if coroutine is not None:
583659
await coroutine
660+
self.types_by_oid[typ_impl.oid] = typ_impl
661+
self.types_by_name[name] = typ_impl
584662
await self.populate_partial_types(conn)
585663
return typ_impl
586664

@@ -598,7 +676,7 @@ cdef class AsyncThinDbObjectTypeCache(BaseThinDbObjectTypeCache):
598676
while self.partial_types:
599677
typ_impl = self.partial_types.pop()
600678
full_name = self._get_full_name(typ_impl)
601-
attrs = await self._lookup_type(conn, full_name)
679+
attrs = await self._lookup_type(conn, full_name, typ_impl)
602680
coroutine = self._populate_type_info(full_name, attrs, typ_impl)
603681
if coroutine is not None:
604682
await coroutine

src/oracledb/impl/thin/pool.pyx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -292,7 +292,7 @@ cdef class BaseThinPoolImpl(BasePoolImpl):
292292
if conn_impl._dbobject_type_cache_num > 0:
293293
cache_num = conn_impl._dbobject_type_cache_num
294294
type_cache = get_dbobject_type_cache(cache_num)
295-
type_cache._clear_meta_cursor()
295+
type_cache._clear_cursors()
296296
if conn_impl._is_pool_extra:
297297
self._extra_conn_impls.remove(conn_impl)
298298
conn_impl._is_pool_extra = False

tests/sql/create_schema.sql

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -318,6 +318,32 @@ create table &main_user..TestJsonCols (
318318
)
319319
/
320320

321+
create table &main_user..TestAllTypes (
322+
NumberValue number,
323+
StringValue varchar2(60),
324+
FixedCharValue char(10),
325+
NStringValue nvarchar2(60),
326+
NFixedCharValue nchar(10),
327+
RawValue raw(16),
328+
IntValue integer,
329+
SmallIntValue smallint,
330+
RealValue real,
331+
DoublePrecisionValue double precision,
332+
FloatValue float,
333+
BinaryFloatValue binary_float,
334+
BinaryDoubleValue binary_double,
335+
DateValue date,
336+
TimestampValue timestamp,
337+
TimestampTZValue timestamp with time zone,
338+
TimestampLTZValue timestamp with local time zone,
339+
CLOBValue clob,
340+
NCLOBValue nclob,
341+
BLOBValue blob,
342+
SubObjectValue &main_user..udt_SubObject,
343+
SubObjectArray &main_user..udt_ObjectArray
344+
)
345+
/
346+
321347
create table &main_user..PlsqlSessionCallbacks (
322348
RequestedTag varchar2(250),
323349
ActualTag varchar2(250),

tests/test_2300_object_var.py

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -751,6 +751,38 @@ def test_2336(self):
751751
with self.assertRaisesFullCode("DPY-2036"):
752752
obj_type([3, 4])
753753

754+
def test_2337(self):
755+
"2337 - test %ROWTYPE with all types"
756+
sub_obj_type = self.conn.gettype("UDT_SUBOBJECT")
757+
sub_arr_type = self.conn.gettype("UDT_OBJECTARRAY")
758+
expected_attrs = [
759+
("NUMBERVALUE", oracledb.DB_TYPE_NUMBER),
760+
("STRINGVALUE", oracledb.DB_TYPE_VARCHAR),
761+
("FIXEDCHARVALUE", oracledb.DB_TYPE_CHAR),
762+
("NSTRINGVALUE", oracledb.DB_TYPE_NVARCHAR),
763+
("NFIXEDCHARVALUE", oracledb.DB_TYPE_NCHAR),
764+
("RAWVALUE", oracledb.DB_TYPE_RAW),
765+
("INTVALUE", oracledb.DB_TYPE_NUMBER),
766+
("SMALLINTVALUE", oracledb.DB_TYPE_NUMBER),
767+
("REALVALUE", oracledb.DB_TYPE_NUMBER),
768+
("DOUBLEPRECISIONVALUE", oracledb.DB_TYPE_NUMBER),
769+
("FLOATVALUE", oracledb.DB_TYPE_NUMBER),
770+
("BINARYFLOATVALUE", oracledb.DB_TYPE_BINARY_FLOAT),
771+
("BINARYDOUBLEVALUE", oracledb.DB_TYPE_BINARY_DOUBLE),
772+
("DATEVALUE", oracledb.DB_TYPE_DATE),
773+
("TIMESTAMPVALUE", oracledb.DB_TYPE_TIMESTAMP),
774+
("TIMESTAMPTZVALUE", oracledb.DB_TYPE_TIMESTAMP_TZ),
775+
("TIMESTAMPLTZVALUE", oracledb.DB_TYPE_TIMESTAMP_LTZ),
776+
("CLOBVALUE", oracledb.DB_TYPE_CLOB),
777+
("NCLOBVALUE", oracledb.DB_TYPE_NCLOB),
778+
("BLOBVALUE", oracledb.DB_TYPE_BLOB),
779+
("SUBOBJECTVALUE", sub_obj_type),
780+
("SUBOBJECTARRAY", sub_arr_type),
781+
]
782+
obj_type = self.conn.gettype("TESTALLTYPES%ROWTYPE")
783+
actual_attrs = [(a.name, a.type) for a in obj_type.attributes]
784+
self.assertEqual(actual_attrs, expected_attrs)
785+
754786

755787
if __name__ == "__main__":
756788
test_env.run_test_cases()

0 commit comments

Comments
 (0)