Skip to content

Commit 7a1e9e0

Browse files
Fixed bug when attempting to convert an integer that cannot be
represented as a native C int value to an Arrow data frame.
1 parent 597a4d4 commit 7a1e9e0

File tree

12 files changed

+74
-14
lines changed

12 files changed

+74
-14
lines changed

doc/src/release_notes.rst

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,8 @@ Common Changes
5757
(`issue 525 <https://github.com/oracle/python-oracledb/issues/525>`__).
5858
#) Pin Cython to 3.1.x instead of 3.1.0 as requested
5959
(`issue 530 <https://github.com/oracle/python-oracledb/issues/530>`__).
60+
#) Fixed bug when attempting to convert an integer that cannot be represented
61+
as a native C ``int`` value to an Arrow data frame.
6062
#) API documentation is now generated from the source code.
6163
#) Internal change: typing_extensions is now a dependency.
6264

src/oracledb/base_impl.pxd

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -958,7 +958,7 @@ cdef struct OracleNumber:
958958
bint is_integer
959959
bint is_max_negative_value
960960
uint8_t num_chars
961-
char_type chars[172]
961+
char_type chars[173]
962962

963963

964964
cdef struct OracleRawBytes:

src/oracledb/base_impl.pyx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,10 +35,11 @@ cimport cython
3535
cimport cpython
3636
cimport cpython.datetime as cydatetime
3737

38+
from libc cimport errno
3839
from libc.stdint cimport int8_t, int16_t, int32_t, int64_t
3940
from libc.stdint cimport uint8_t, uint16_t, uint32_t, uint64_t
4041
from libc.stdint cimport UINT8_MAX, UINT16_MAX, UINT32_MAX, UINT64_MAX
41-
from libc.stdlib cimport atoi, atof
42+
from libc.stdlib cimport strtod, strtoll
4243
from libc.string cimport memcpy
4344
from cpython cimport array
4445
from cpython.conversion cimport PyOS_snprintf

src/oracledb/errors.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -364,6 +364,8 @@ def _raise_not_supported(feature: str) -> None:
364364
ERR_EXCEEDED_IDLE_TIME = 4033
365365
ERR_INVALID_PASSWORD_TYPE = 4034
366366
ERR_INVALID_SERVER_RESPONSE = 4035
367+
ERR_CANNOT_CONVERT_TO_ARROW_INTEGER = 4036
368+
ERR_CANNOT_CONVERT_TO_ARROW_DOUBLE = 4037
367369

368370
# error numbers that result in InternalError
369371
ERR_MESSAGE_TYPE_UNKNOWN = 5000
@@ -559,6 +561,12 @@ def _raise_not_supported(feature: str) -> None:
559561
"insufficient to hold {required_buffer_len} bytes"
560562
),
561563
ERR_CALL_TIMEOUT_EXCEEDED: "call timeout of {timeout} ms exceeded",
564+
ERR_CANNOT_CONVERT_TO_ARROW_DOUBLE: (
565+
"{value} cannot be converted to an Arrow double"
566+
),
567+
ERR_CANNOT_CONVERT_TO_ARROW_INTEGER: (
568+
"{value} cannot be converted to an Arrow integer"
569+
),
562570
ERR_CANNOT_PARSE_CONNECT_STRING: 'cannot parse connect string "{data}"',
563571
ERR_COLUMN_TRUNCATED: (
564572
"column truncated to {col_value_len} {unit}. "

src/oracledb/impl/base/converters.pyx

Lines changed: 18 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -231,20 +231,34 @@ cdef int convert_number_to_arrow_double(ArrowArrayImpl array_impl,
231231
"""
232232
Converts a NUMBER value stored in the buffer to Arrow DOUBLE.
233233
"""
234-
cdef OracleNumber *value = &buffer.as_number
234+
cdef:
235+
OracleNumber *value = &buffer.as_number
236+
double double_value
235237
if value.is_max_negative_value:
236238
array_impl.append_double(-1.0e126)
237239
else:
238-
array_impl.append_double(atof(value.chars[:value.num_chars]))
240+
errno.errno = 0
241+
double_value = strtod((<const char*> value.chars), NULL)
242+
if errno.errno != 0:
243+
errors._raise_err(errors.ERR_CANNOT_CONVERT_TO_ARROW_DOUBLE,
244+
value=value.chars[:value.num_chars].decode())
245+
array_impl.append_double(double_value)
239246

240247

241248
cdef int convert_number_to_arrow_int64(ArrowArrayImpl array_impl,
242249
OracleDataBuffer *buffer) except -1:
243250
"""
244251
Converts a NUMBER value stored in the buffer to Arrow INT64.
245252
"""
246-
cdef OracleNumber *value = &buffer.as_number
247-
array_impl.append_int64(atoi(value.chars[:value.num_chars]))
253+
cdef:
254+
OracleNumber *value = &buffer.as_number
255+
int64_t int64_value
256+
errno.errno = 0
257+
int64_value = strtoll((<const char*> value.chars), NULL, 0)
258+
if errno.errno != 0:
259+
errors._raise_err(errors.ERR_CANNOT_CONVERT_TO_ARROW_INTEGER,
260+
value=value.chars[:value.num_chars].decode())
261+
array_impl.append_int64(int64_value)
248262

249263

250264
cdef object convert_number_to_python_decimal(OracleDataBuffer *buffer):

src/oracledb/impl/base/decoders.pyx

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -196,6 +196,7 @@ cdef int decode_number(const uint8_t* ptr, ssize_t num_bytes,
196196
if is_positive:
197197
output.num_chars = 1
198198
output.chars[0] = 48 # zero
199+
output.chars[1] = 0 # null terminator
199200
else:
200201
output.is_max_negative_value = True
201202
return 0
@@ -270,6 +271,9 @@ cdef int decode_number(const uint8_t* ptr, ssize_t num_bytes,
270271
output.chars[output.num_chars] = 48 # zero
271272
output.num_chars += 1
272273

274+
# include null terminator for use by strtoll() and strtoull()
275+
output.chars[output.num_chars] = 0
276+
273277

274278
cdef inline uint16_t decode_uint16be(const char_type *buf):
275279
"""

src/oracledb/impl/thick/var.pyx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -490,7 +490,7 @@ cdef class ThickVarImpl(BaseVarImpl):
490490
ora_data.buffer.as_number.is_integer = \
491491
memchr(as_bytes.ptr, b'.', as_bytes.length) == NULL;
492492
memcpy(ora_data.buffer.as_number.chars, as_bytes.ptr,
493-
as_bytes.length);
493+
as_bytes.length + 1);
494494
ora_data.buffer.as_number.num_chars = as_bytes.length;
495495
elif ora_type_num == DPI_ORACLE_TYPE_VECTOR:
496496
vector = _convert_vector_to_python(data.value.asVector)

tests/sql/create_schema.sql

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -410,7 +410,8 @@ create table &main_user..TestDataframe (
410410
CreditScore number(3, 0),
411411
LastUpdated timestamp,
412412
DecimalData number(15, 4),
413-
IntegerData number(20),
413+
IntegerData number(15),
414+
LongIntegerData number(38),
414415
FloatData binary_float,
415416
DoubleData binary_double,
416417
RawData raw(100),

tests/test_8000_dataframe.py

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1868,6 +1868,21 @@ def test_8076(self):
18681868
]
18691869
self.__test_df_interop(data)
18701870

1871+
def test_8077(self):
1872+
"8077 - test fetching large integers"
1873+
data = (-(2**40), 2**41)
1874+
ora_df = self.conn.fetch_df_all(
1875+
"""
1876+
select
1877+
cast(:1 as number(15)),
1878+
cast(:2 as number(15))
1879+
from dual
1880+
""",
1881+
data,
1882+
)
1883+
fetched_df = pyarrow.table(ora_df).to_pandas()
1884+
self.assertEqual([data], self.__get_data_from_df(fetched_df))
1885+
18711886

18721887
if __name__ == "__main__":
18731888
test_env.run_test_cases()

tests/test_8100_dataframe_async.py

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1638,6 +1638,21 @@ async def test_8165(self):
16381638
]
16391639
await self.__test_df_interop(data)
16401640

1641+
async def test_8166(self):
1642+
"8166 - test fetching large integers"
1643+
data = (-(2**40), 2**41)
1644+
ora_df = await self.conn.fetch_df_all(
1645+
"""
1646+
select
1647+
cast(:1 as number(15)),
1648+
cast(:2 as number(15))
1649+
from dual
1650+
""",
1651+
data,
1652+
)
1653+
fetched_df = pyarrow.table(ora_df).to_pandas()
1654+
self.assertEqual([data], self.__get_data_from_df(fetched_df))
1655+
16411656

16421657
if __name__ == "__main__":
16431658
test_env.run_test_cases()

0 commit comments

Comments
 (0)