Skip to content

Commit 8211232

Browse files
Validate database objects meet maximum size constraints.
1 parent 9227b58 commit 8211232

File tree

8 files changed

+151
-16
lines changed

8 files changed

+151
-16
lines changed

‎doc/src/release_notes.rst‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,10 @@ Common Changes
7979
:data:`FetchInfo.type_code` for data of this type was
8080
:data:`~oracledb.DB_TYPE_LONG` in Thick mode and
8181
:data:`~oracledb.DB_TYPE_OBJECT` in Thin mode.
82+
#) Attribute and element values of :ref:`Oracle Object <dbobject>` instances
83+
that contain strings or bytes now have their maximum size constraints
84+
checked. Errors ``DPY-2043`` (attributes) and ``DPY-2044`` (element values)
85+
are now raised when constraints are violated.
8286
#) Attribute and element values of :ref:`Oracle Object <dbobject>` instances
8387
that are numbers are now returned as integers if the precision and scale
8488
allow for it. This is the same way that numbers are fetched from the

‎src/oracledb/base_impl.pxd‎

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -386,22 +386,33 @@ cdef class BaseDbObjectTypeImpl:
386386
readonly dict attrs_by_name
387387
readonly DbType element_dbtype
388388
readonly BaseDbObjectTypeImpl element_objtype
389+
readonly int8_t element_precision
390+
readonly int8_t element_scale
391+
readonly uint32_t element_max_size
389392
readonly BaseConnImpl _conn_impl
390393
int _element_preferred_num_type
391394

395+
cpdef str _get_fqn(self)
396+
392397

393398
cdef class BaseDbObjectAttrImpl:
394399
cdef:
395400
readonly str name
396401
readonly DbType dbtype
397402
readonly BaseDbObjectTypeImpl objtype
403+
readonly int8_t precision
404+
readonly int8_t scale
405+
readonly uint32_t max_size
398406
int _preferred_num_type
399407

400408

401409
cdef class BaseDbObjectImpl:
402410
cdef:
403411
readonly BaseDbObjectTypeImpl type
404412

413+
cdef int _check_max_size(self, object value, uint32_t max_size,
414+
ssize_t* actual_size, bint* violated) except -1
415+
405416

406417
cdef class BaseSodaDbImpl:
407418
cdef:

‎src/oracledb/dbobject.py‎

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -282,9 +282,7 @@ def _get_full_name(self):
282282
"""
283283
Returns the full name of the type.
284284
"""
285-
if self.package_name is not None:
286-
return f"{self.schema}.{self.package_name}.{self.name}"
287-
return f"{self.schema}.{self.name}"
285+
return self._impl._get_fqn()
288286

289287
@property
290288
def attributes(self) -> list:

‎src/oracledb/errors.py‎

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -225,6 +225,8 @@ def _raise_from_string(exc_type: Exception, message: str) -> None:
225225
ERR_EXECUTE_MODE_ONLY_FOR_DML = 2040
226226
ERR_MISSING_QUOTE_IN_STRING = 2041
227227
ERR_MISSING_QUOTE_IN_IDENTIFIER = 2042
228+
ERR_DBOBJECT_ATTR_MAX_SIZE_VIOLATED = 2043
229+
ERR_DBOBJECT_ELEMENT_MAX_SIZE_VIOLATED = 2044
228230

229231
# error numbers that result in NotSupportedError
230232
ERR_TIME_NOT_SUPPORTED = 3000
@@ -386,6 +388,14 @@ def _raise_from_string(exc_type: Exception, message: str) -> None:
386388
),
387389
ERR_CONTENT_INVALID_AFTER_NUMBER: "invalid number (content after number)",
388390
ERR_CURSOR_NOT_OPEN: "cursor is not open",
391+
ERR_DBOBJECT_ATTR_MAX_SIZE_VIOLATED: (
392+
"attribute {attr_name} of type {type_name} exceeds its maximum size "
393+
"(actual: {actual_size}, maximum: {max_size})"
394+
),
395+
ERR_DBOBJECT_ELEMENT_MAX_SIZE_VIOLATED: (
396+
"element {index} of type {type_name} exceeds its maximum size "
397+
"(actual: {actual_size}, maximum: {max_size})"
398+
),
389399
ERR_DB_TYPE_NOT_SUPPORTED: 'database type "{name}" is not supported',
390400
ERR_DUPLICATED_PARAMETER: (
391401
'"{deprecated_name}" and "{new_name}" cannot be specified together'

‎src/oracledb/impl/base/dbobject.pyx‎

Lines changed: 55 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -31,14 +31,39 @@
3131

3232
cdef class BaseDbObjectImpl:
3333

34+
cdef int _check_max_size(self, object value, uint32_t max_size,
35+
ssize_t* actual_size, bint* violated) except -1:
36+
"""
37+
Checks to see if the maximum size has been violated.
38+
"""
39+
violated[0] = False
40+
if max_size > 0:
41+
if isinstance(value, str):
42+
actual_size[0] = len((<str> value).encode())
43+
else:
44+
actual_size[0] = len(<bytes> value)
45+
if actual_size[0] > max_size:
46+
violated[0] = True
47+
3448
def append(self, object value):
3549
"""
3650
Appends a value to the collection after first checking to see if the
3751
value is acceptable.
3852
"""
39-
cdef BaseConnImpl conn_impl = self.type._conn_impl
53+
cdef:
54+
BaseConnImpl conn_impl = self.type._conn_impl
55+
ssize_t actual_size
56+
bint violated
4057
value = conn_impl._check_value(self.type.element_dbtype,
4158
self.type.element_objtype, value, NULL)
59+
self._check_max_size(value, self.type.element_max_size, &actual_size,
60+
&violated)
61+
if violated:
62+
errors._raise_err(errors.ERR_DBOBJECT_ELEMENT_MAX_SIZE_VIOLATED,
63+
index=self.get_size(),
64+
type_name=self.type._get_fqn(),
65+
actual_size=actual_size,
66+
max_size=self.type.element_max_size)
4267
self.append_checked(value)
4368

4469
@utils.CheckImpls("appending a value to a collection")
@@ -90,8 +115,17 @@ cdef class BaseDbObjectImpl:
90115
Sets the attribute value after first checking to see if the value is
91116
acceptable.
92117
"""
93-
cdef BaseConnImpl conn_impl = self.type._conn_impl
118+
cdef:
119+
BaseConnImpl conn_impl = self.type._conn_impl
120+
ssize_t actual_size
121+
bint violated
94122
value = conn_impl._check_value(attr.dbtype, attr.objtype, value, NULL)
123+
self._check_max_size(value, attr.max_size, &actual_size, &violated)
124+
if violated:
125+
errors._raise_err(errors.ERR_DBOBJECT_ATTR_MAX_SIZE_VIOLATED,
126+
attr_name=attr.name,
127+
type_name=self.type._get_fqn(),
128+
actual_size=actual_size, max_size=attr.max_size)
95129
self.set_attr_value_checked(attr, value)
96130

97131
@utils.CheckImpls("setting an attribute value")
@@ -103,9 +137,19 @@ cdef class BaseDbObjectImpl:
103137
Sets the element value after first checking to see if the value is
104138
acceptable.
105139
"""
106-
cdef BaseConnImpl conn_impl = self.type._conn_impl
140+
cdef:
141+
BaseConnImpl conn_impl = self.type._conn_impl
142+
ssize_t actual_size
143+
bint violated
107144
value = conn_impl._check_value(self.type.element_dbtype,
108145
self.type.element_objtype, value, NULL)
146+
self._check_max_size(value, self.type.element_max_size, &actual_size,
147+
&violated)
148+
if violated:
149+
errors._raise_err(errors.ERR_DBOBJECT_ELEMENT_MAX_SIZE_VIOLATED,
150+
index=index, type_name=self.type._get_fqn(),
151+
actual_size=actual_size,
152+
max_size=self.type.element_max_size)
109153
self.set_element_by_index_checked(index, value)
110154

111155
@utils.CheckImpls("setting an element of a collection")
@@ -130,6 +174,14 @@ cdef class BaseDbObjectTypeImpl:
130174
and other.name == self.name
131175
return NotImplemented
132176

177+
cpdef str _get_fqn(self):
178+
"""
179+
Return the fully qualified name of the type.
180+
"""
181+
if self.package_name is not None:
182+
return f"{self.schema}.{self.package_name}.{self.name}"
183+
return f"{self.schema}.{self.name}"
184+
133185
@utils.CheckImpls("creating a new object")
134186
def create_new_object(self):
135187
pass

‎src/oracledb/impl/thick/dbobject.pyx‎

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -287,6 +287,9 @@ cdef class ThickDbObjectAttrImpl(BaseDbObjectAttrImpl):
287287
_raise_from_odpi()
288288
impl.name = info.name[:info.nameLength].decode()
289289
impl.dbtype = DbType._from_num(info.typeInfo.oracleTypeNum)
290+
impl.precision = info.typeInfo.precision
291+
impl.scale = info.typeInfo.scale
292+
impl.max_size = info.typeInfo.dbSizeInBytes
290293
impl._preferred_num_type = \
291294
get_preferred_num_type(info.typeInfo.precision,
292295
info.typeInfo.scale)
@@ -338,6 +341,9 @@ cdef class ThickDbObjectTypeImpl(BaseDbObjectTypeImpl):
338341
if impl.is_collection:
339342
dbtype = DbType._from_num(info.elementTypeInfo.oracleTypeNum)
340343
impl.element_dbtype = dbtype
344+
impl.element_precision = info.elementTypeInfo.precision
345+
impl.element_scale = info.elementTypeInfo.scale
346+
impl.element_max_size = info.elementTypeInfo.dbSizeInBytes
341347
impl._element_preferred_num_type = \
342348
get_preferred_num_type(info.elementTypeInfo.precision,
343349
info.elementTypeInfo.scale)

‎src/oracledb/impl/thin/dbobject_cache.pyx‎

Lines changed: 23 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -234,7 +234,9 @@ cdef class BaseThinDbObjectTypeCache:
234234
typ_impl.collection_flags = TNS_OBJ_HAS_INDEXES
235235
buf.skip_to(pos)
236236
typ_impl.element_dbtype = self._parse_tds_attr(
237-
buf, &typ_impl._element_preferred_num_type
237+
buf, &typ_impl.element_precision, &typ_impl.element_scale,
238+
&typ_impl.element_max_size,
239+
&typ_impl._element_preferred_num_type
238240
)
239241
if typ_impl.element_dbtype is DB_TYPE_CLOB:
240242
return self._get_element_type_clob(typ_impl)
@@ -244,16 +246,22 @@ cdef class BaseThinDbObjectTypeCache:
244246
# handle objects with attributes
245247
else:
246248
for i, attr_impl in enumerate(typ_impl.attrs):
247-
self._parse_tds_attr(buf, &attr_impl._preferred_num_type)
249+
self._parse_tds_attr(buf, &attr_impl.precision,
250+
&attr_impl.scale, &attr_impl.max_size,
251+
&attr_impl._preferred_num_type)
248252

249-
cdef DbType _parse_tds_attr(self, TDSBuffer buf, int* preferred_num_type):
253+
cdef DbType _parse_tds_attr(self, TDSBuffer buf, int8_t* precision,
254+
int8_t* scale, uint32_t *max_size,
255+
int* preferred_num_type):
250256
"""
251257
Parses a TDS attribute from the buffer.
252258
"""
253259
cdef:
254260
uint8_t attr_type, ora_type_num = 0, csfrm = 0
261+
int8_t temp_precision, temp_scale
255262
int temp_preferred_num_type
256-
int8_t precision, scale
263+
uint32_t temp_max_size
264+
uint16_t temp16
257265

258266
# skip until a type code that is of interest
259267
while True:
@@ -266,14 +274,16 @@ cdef class BaseThinDbObjectTypeCache:
266274
# process the type code
267275
if attr_type == TNS_OBJ_TDS_TYPE_NUMBER:
268276
ora_type_num = TNS_DATA_TYPE_NUMBER
269-
buf.read_sb1(&precision)
270-
buf.read_sb1(&scale)
271-
preferred_num_type[0] = get_preferred_num_type(precision, scale)
277+
buf.read_sb1(precision)
278+
buf.read_sb1(scale)
279+
preferred_num_type[0] = \
280+
get_preferred_num_type(precision[0], scale[0])
272281
elif attr_type == TNS_OBJ_TDS_TYPE_FLOAT:
273282
ora_type_num = TNS_DATA_TYPE_NUMBER
274283
buf.skip_raw_bytes(1) # precision
275284
elif attr_type in (TNS_OBJ_TDS_TYPE_VARCHAR, TNS_OBJ_TDS_TYPE_CHAR):
276-
buf.skip_raw_bytes(2) # maximum length
285+
buf.read_uint16(&temp16) # maximum length
286+
max_size[0] = temp16
277287
buf.read_ub1(&csfrm)
278288
csfrm = csfrm & 0x7f
279289
buf.skip_raw_bytes(2) # character set
@@ -282,7 +292,8 @@ cdef class BaseThinDbObjectTypeCache:
282292
else:
283293
ora_type_num = TNS_DATA_TYPE_CHAR
284294
elif attr_type == TNS_OBJ_TDS_TYPE_RAW:
285-
buf.skip_raw_bytes(2) # maximum length
295+
buf.read_uint16(&temp16) # maximum length
296+
max_size[0] = temp16
286297
ora_type_num = TNS_DATA_TYPE_RAW
287298
elif attr_type == TNS_OBJ_TDS_TYPE_BINARY_FLOAT:
288299
ora_type_num = TNS_DATA_TYPE_BINARY_FLOAT
@@ -311,7 +322,9 @@ cdef class BaseThinDbObjectTypeCache:
311322
buf.skip_raw_bytes(5) # offset and code
312323
elif attr_type == TNS_OBJ_TDS_TYPE_START_EMBED_ADT:
313324
ora_type_num = TNS_DATA_TYPE_INT_NAMED
314-
while self._parse_tds_attr(buf, &temp_preferred_num_type):
325+
while self._parse_tds_attr(buf, &temp_precision, &temp_scale,
326+
&temp_max_size,
327+
&temp_preferred_num_type):
315328
pass
316329
elif attr_type == TNS_OBJ_TDS_TYPE_END_EMBED_ADT:
317330
return None

‎tests/test_2300_object_var.py‎

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -730,6 +730,47 @@ def test_2332_validate_invalid_attr_name(self):
730730
with self.assertRaises(AttributeError):
731731
obj.MISSING
732732

733+
def test_2333_validate_string_attr(self):
734+
"2333 - test validating a string attribute"
735+
typ = self.conn.gettype("UDT_OBJECT")
736+
obj = typ.newobject()
737+
for attr_name, max_size in [
738+
("STRINGVALUE", 60),
739+
("FIXEDCHARVALUE", 10),
740+
("NSTRINGVALUE", 120),
741+
("NFIXEDCHARVALUE", 20),
742+
("RAWVALUE", 16),
743+
]:
744+
with self.subTest(attr_name=attr_name, max_size=max_size):
745+
value = "A" * max_size
746+
setattr(obj, attr_name, value)
747+
value += "X"
748+
self.assertRaisesRegex(
749+
oracledb.ProgrammingError,
750+
"^DPY-2043:",
751+
setattr,
752+
obj,
753+
attr_name,
754+
value,
755+
)
756+
757+
def test_2334_validate_string_element_value(self):
758+
"2334 - test validating a string element value"
759+
typ = self.conn.gettype("PKG_TESTSTRINGARRAYS.UDT_STRINGLIST")
760+
obj = typ.newobject()
761+
obj.append("A" * 100)
762+
self.assertRaisesRegex(
763+
oracledb.ProgrammingError, "^DPY-2044:", obj.append, "A" * 101
764+
)
765+
obj.append("B" * 100)
766+
self.assertRaisesRegex(
767+
oracledb.ProgrammingError,
768+
"^DPY-2044:",
769+
obj.setelement,
770+
2,
771+
"C" * 101,
772+
)
773+
733774

734775
if __name__ == "__main__":
735776
test_env.run_test_cases()

0 commit comments

Comments
 (0)