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)