Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 8 additions & 2 deletions redshift_connector/utils/type_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
from datetime import time
from datetime import timedelta as Timedelta
from datetime import timezone as Timezone
from decimal import Decimal
from decimal import Context, Decimal
from enum import Enum
from json import loads
from struct import Struct
Expand Down Expand Up @@ -225,6 +225,9 @@ def intervald2s_send_integer(v: IntervalDayToSecond) -> bytes:
return typing.cast(bytes, q_pack(microseconds))


_NUMERIC_BINARY_CONTEXT: Context = Context(prec=39)


def numeric_in_binary(data: bytes, offset: int, length: int, scale: int) -> Decimal:
raw_value: int

Expand All @@ -233,7 +236,10 @@ def numeric_in_binary(data: bytes, offset: int, length: int, scale: int) -> Deci
else:
raise Exception("Malformed column value of type numeric received")

return Decimal(raw_value).scaleb(-1 * scale)
# scaleb() rounds to the current decimal context (28 digits by default),
# which silently truncated NUMERIC(38, s) values. A 16-byte value has at
# most 39 digits, so this context keeps every digit.
return Decimal(raw_value).scaleb(-1 * scale, context=_NUMERIC_BINARY_CONTEXT)


def numeric_to_float_binary(data: bytes, offset: int, length: int, scale: int) -> float:
Expand Down
20 changes: 19 additions & 1 deletion test/unit/datatype/test_data_in.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import typing
from datetime import date, datetime, time, timedelta, timezone
from decimal import Decimal
from decimal import Context, Decimal
from enum import Enum, auto
from math import isclose

Expand Down Expand Up @@ -492,3 +492,21 @@ def test_numeric_to_float_binary_raises_for_invalid_length(length):
length, # invalid length
8,
)


@pytest.mark.parametrize(
"value, scale, length",
[
(Decimal("12345678901234567890.123456789012345678"), 18, 16),
(Decimal("-99999999999999999999.999999999999999999"), 18, 16),
(Decimal("0.000000000000000001"), 18, 16),
(Decimal("170141183460469231731687303715884105727"), 0, 16),
(Decimal("-12345.67"), 2, 8),
],
)
def test_numeric_in_binary_is_exact_beyond_28_digits(value, scale, length):
raw_value = int(value.scaleb(scale, context=Context(prec=60)))
data = raw_value.to_bytes(length, byteorder="big", signed=True)
result = Datatypes.numeric_binary(data, 0, length, scale)
assert result == value
assert result.as_tuple().exponent == -scale