Skip to content

Commit 575a67b

Browse files
committed
feat(datatype, interval): support interval, timedelta
1 parent 627ea84 commit 575a67b

File tree

6 files changed

+263
-162
lines changed

6 files changed

+263
-162
lines changed

redshift_connector/interval.py

Lines changed: 89 additions & 85 deletions
Original file line numberDiff line numberDiff line change
@@ -1,85 +1,89 @@
1-
# from redshift_connector.config import max_int4, max_int8, min_int4, min_int8
2-
#
3-
#
4-
# class Interval:
5-
# """An Interval represents a measurement of time. In PostgreSQL, an
6-
# interval is defined in the measure of months, days, and microseconds; as
7-
# such, the interval type represents the same information.
8-
#
9-
# Note that values of the :attr:`microseconds`, :attr:`days` and
10-
# :attr:`months` properties are independently measured and cannot be
11-
# converted to each other. A month may be 28, 29, 30, or 31 days, and a day
12-
# may occasionally be lengthened slightly by a leap second.
13-
#
14-
# .. attribute:: microseconds
15-
#
16-
# Measure of microseconds in the interval.
17-
#
18-
# The microseconds value is constrained to fit into a signed 64-bit
19-
# integer. Any attempt to set a value too large or too small will result
20-
# in an OverflowError being raised.
21-
#
22-
# .. attribute:: days
23-
#
24-
# Measure of days in the interval.
25-
#
26-
# The days value is constrained to fit into a signed 32-bit integer.
27-
# Any attempt to set a value too large or too small will result in an
28-
# OverflowError being raised.
29-
#
30-
# .. attribute:: months
31-
#
32-
# Measure of months in the interval.
33-
#
34-
# The months value is constrained to fit into a signed 32-bit integer.
35-
# Any attempt to set a value too large or too small will result in an
36-
# OverflowError being raised.
37-
# """
38-
#
39-
# def __init__(self: 'Interval', microseconds: int = 0, days: int = 0, months: int = 0) -> None:
40-
# self.microseconds = microseconds
41-
# self.days = days
42-
# self.months = months
43-
#
44-
# def _setMicroseconds(self: 'Interval', value: int) -> None:
45-
# if not isinstance(value, int):
46-
# raise TypeError("microseconds must be an integer type")
47-
# elif not (min_int8 < value < max_int8):
48-
# raise OverflowError(
49-
# "microseconds must be representable as a 64-bit integer")
50-
# else:
51-
# self._microseconds = value
52-
#
53-
# def _setDays(self: 'Interval', value: int) -> None:
54-
# if not isinstance(value, int):
55-
# raise TypeError("days must be an integer type")
56-
# elif not (min_int4 < value < max_int4):
57-
# raise OverflowError(
58-
# "days must be representable as a 32-bit integer")
59-
# else:
60-
# self._days = value
61-
#
62-
# def _setMonths(self: 'Interval', value: int) -> None:
63-
# if not isinstance(value, int):
64-
# raise TypeError("months must be an integer type")
65-
# elif not (min_int4 < value < max_int4):
66-
# raise OverflowError(
67-
# "months must be representable as a 32-bit integer")
68-
# else:
69-
# self._months = value
70-
#
71-
# microseconds = property(lambda self: self._microseconds, _setMicroseconds)
72-
# days = property(lambda self: self._days, _setDays)
73-
# months = property(lambda self: self._months, _setMonths)
74-
#
75-
# def __repr__(self: 'Interval') -> str:
76-
# return "<Interval %s months %s days %s microseconds>" % (
77-
# self.months, self.days, self.microseconds)
78-
#
79-
# def __eq__(self: 'Interval', other: object) -> bool:
80-
# return other is not None and isinstance(other, Interval) and \
81-
# self.months == other.months and self.days == other.days and \
82-
# self.microseconds == other.microseconds
83-
#
84-
# def __neq__(self: 'Interval', other: 'Interval') -> bool:
85-
# return not self.__eq__(other)
1+
from redshift_connector.config import max_int4, max_int8, min_int4, min_int8
2+
3+
4+
class Interval:
5+
"""An Interval represents a measurement of time. In PostgreSQL, an
6+
interval is defined in the measure of months, days, and microseconds; as
7+
such, the interval type represents the same information.
8+
9+
Note that values of the :attr:`microseconds`, :attr:`days` and
10+
:attr:`months` properties are independently measured and cannot be
11+
converted to each other. A month may be 28, 29, 30, or 31 days, and a day
12+
may occasionally be lengthened slightly by a leap second.
13+
14+
.. attribute:: microseconds
15+
16+
Measure of microseconds in the interval.
17+
18+
The microseconds value is constrained to fit into a signed 64-bit
19+
integer. Any attempt to set a value too large or too small will result
20+
in an OverflowError being raised.
21+
22+
.. attribute:: days
23+
24+
Measure of days in the interval.
25+
26+
The days value is constrained to fit into a signed 32-bit integer.
27+
Any attempt to set a value too large or too small will result in an
28+
OverflowError being raised.
29+
30+
.. attribute:: months
31+
32+
Measure of months in the interval.
33+
34+
The months value is constrained to fit into a signed 32-bit integer.
35+
Any attempt to set a value too large or too small will result in an
36+
OverflowError being raised.
37+
"""
38+
39+
def __init__(self: "Interval", microseconds: int = 0, days: int = 0, months: int = 0) -> None:
40+
self.microseconds = microseconds
41+
self.days = days
42+
self.months = months
43+
44+
def _setMicroseconds(self: "Interval", value: int) -> None:
45+
if not isinstance(value, int):
46+
raise TypeError("microseconds must be an integer type")
47+
elif not (min_int8 < value < max_int8):
48+
raise OverflowError("microseconds must be representable as a 64-bit integer")
49+
else:
50+
self._microseconds = value
51+
52+
def _setDays(self: "Interval", value: int) -> None:
53+
if not isinstance(value, int):
54+
raise TypeError("days must be an integer type")
55+
elif not (min_int4 < value < max_int4):
56+
raise OverflowError("days must be representable as a 32-bit integer")
57+
else:
58+
self._days = value
59+
60+
def _setMonths(self: "Interval", value: int) -> None:
61+
if not isinstance(value, int):
62+
raise TypeError("months must be an integer type")
63+
elif not (min_int4 < value < max_int4):
64+
raise OverflowError("months must be representable as a 32-bit integer")
65+
else:
66+
self._months = value
67+
68+
microseconds = property(lambda self: self._microseconds, _setMicroseconds)
69+
days = property(lambda self: self._days, _setDays)
70+
months = property(lambda self: self._months, _setMonths)
71+
72+
def __repr__(self: "Interval") -> str:
73+
return "<Interval %s months %s days %s microseconds>" % (self.months, self.days, self.microseconds)
74+
75+
def __eq__(self: "Interval", other: object) -> bool:
76+
return (
77+
other is not None
78+
and isinstance(other, Interval)
79+
and self.months == other.months
80+
and self.days == other.days
81+
and self.microseconds == other.microseconds
82+
)
83+
84+
def __neq__(self: "Interval", other: "Interval") -> bool:
85+
return not self.__eq__(other)
86+
87+
def total_seconds(self: "Interval") -> float:
88+
"""Total seconds in the Interval, excluding month field."""
89+
return ((self.days * 86400) * 10 ** 6 + self.microseconds) / 10 ** 6

redshift_connector/utils/type_utils.py

Lines changed: 21 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
_client_encoding,
2222
timegm,
2323
)
24+
from redshift_connector.interval import Interval
2425
from redshift_connector.pg_types import (
2526
PGEnum,
2627
PGJson,
@@ -115,6 +116,7 @@ def pack_funcs(fmt: str) -> typing.Tuple[typing.Callable, typing.Callable]:
115116
f_pack, f_unpack = pack_funcs("f")
116117
iii_pack, iii_unpack = pack_funcs("iii")
117118
ii_pack, ii_unpack = pack_funcs("ii")
119+
qhh_pack, qhh_unpack = pack_funcs("qhh")
118120
qii_pack, qii_unpack = pack_funcs("qii")
119121
dii_pack, dii_unpack = pack_funcs("dii")
120122
ihihih_pack, ihihih_unpack = pack_funcs("ihihih")
@@ -235,19 +237,15 @@ def timestamptz_recv_integer(data: bytes, offset: int, length: int) -> typing.Un
235237
return Datetime.max
236238

237239

238-
# def interval_send_integer(v: typing.Union[Interval, Timedelta]) -> bytes:
239-
# microseconds: int = v.microseconds
240-
# try:
241-
# microseconds += int(v.seconds * 1e6) # type: ignore
242-
# except AttributeError:
243-
# pass
244-
#
245-
# try:
246-
# months = v.months # type: ignore
247-
# except AttributeError:
248-
# months = 0
249-
#
250-
# return typing.cast(bytes, qii_pack(microseconds, v.days, months))
240+
def interval_send_integer(v: typing.Union[Timedelta, Interval]) -> bytes:
241+
microseconds: int = int(v.total_seconds() * 1e6)
242+
243+
try:
244+
months = v.months # type: ignore
245+
except AttributeError:
246+
months = 0
247+
248+
return typing.cast(bytes, qhh_pack(microseconds, 0, months))
251249

252250

253251
glbls: typing.Dict[str, type] = {"Decimal": Decimal}
@@ -292,15 +290,13 @@ def numeric_in(data: bytes, offset: int, length: int) -> Decimal:
292290
# return UUID(bytes=data[offset:offset+length])
293291

294292

295-
# def interval_recv_integer(data: bytes, offset: int, length: int) -> typing.Union[Timedelta, Interval]:
296-
# microseconds, days, months = typing.cast(
297-
# typing.Tuple[int, ...], qii_unpack(data, offset)
298-
# )
299-
# if months == 0:
300-
# seconds, micros = divmod(microseconds, 1e6)
301-
# return Timedelta(days, seconds, micros)
302-
# else:
303-
# return Interval(microseconds, days, months)
293+
def interval_recv_integer(data: bytes, offset: int, length: int) -> typing.Union[Timedelta, Interval]:
294+
microseconds, days, months = typing.cast(typing.Tuple[int, ...], qhh_unpack(data, offset))
295+
seconds, micros = divmod(microseconds, 1e6)
296+
if months != 0:
297+
return Interval(microseconds, days, months)
298+
else:
299+
return Timedelta(days, seconds, micros)
304300

305301

306302
def timetz_recv_binary(data: bytes, offset: int, length: int) -> time:
@@ -630,7 +626,7 @@ def varbytehex_recv(data: bytes, idx: int, length: int) -> str:
630626
TIMESTAMP: (FC_BINARY, timestamp_recv_integer), # timestamp
631627
TIMESTAMPTZ: (FC_BINARY, timestamptz_recv_integer), # timestamptz
632628
TIMETZ: (FC_BINARY, timetz_recv_binary), # timetz
633-
# 1186: (FC_BINARY, interval_recv_integer),
629+
INTERVAL: (FC_BINARY, interval_recv_integer),
634630
# 1231: (FC_TEXT, array_in), # NUMERIC[]
635631
# 1263: (FC_BINARY, array_recv), # cstring[]
636632
NUMERIC: (FC_BINARY, numeric_in_binary), # NUMERIC
@@ -691,8 +687,8 @@ def numeric_out(d: Decimal) -> bytes:
691687
TIMESTAMPTZ: (TIMESTAMPTZ, FC_BINARY, timestamptz_send_integer),
692688
PGJson: (JSON, FC_TEXT, text_out),
693689
# PGJsonb: (3802, FC_TEXT, text_out),
694-
# Timedelta: (1186, FC_BINARY, interval_send_integer),
695-
# Interval: (1186, FC_BINARY, interval_send_integer),
690+
Timedelta: (INTERVAL, FC_BINARY, interval_send_integer), # interval
691+
Interval: (1186, FC_BINARY, interval_send_integer),
696692
Decimal: (NUMERIC, FC_TEXT, numeric_out), # Decimal
697693
PGTsvector: (3614, FC_TEXT, text_out),
698694
# UUID: (2950, FC_BINARY, uuid_send), # uuid

test/integration/datatype/test_datatypes.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -94,7 +94,7 @@ def test_redshift_specific_recv_support(db_kwargs, _input, client_protocol):
9494
results: typing.Tuple = cursor.fetchall()
9595
assert len(results) == 1
9696
assert len(results[0]) == 1
97-
assert results[0][0] == bytes(exp_val, encoding="utf-8").hex()
97+
assert results[0][0] == exp_val
9898

9999

100100
@pytest.mark.skip(reason="manual")

test/integration/test_typeobjects.py

Lines changed: 0 additions & 50 deletions
This file was deleted.

test/unit/datatype/test_data_in.py

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,13 @@
11
import typing
2-
from datetime import date, datetime, time, timezone
2+
from datetime import date, datetime, time, timedelta, timezone
33
from decimal import Decimal
44
from enum import Enum, auto
55
from math import isclose
66

77
import pytest # type: ignore
88

99
import redshift_connector
10+
from redshift_connector.interval import Interval
1011
from redshift_connector.utils import type_utils
1112

1213

@@ -36,6 +37,7 @@ class Datatypes(Enum):
3637
text_array: typing.Callable = type_utils.array_recv_text
3738
text_array_binary: typing.Callable = type_utils.array_recv_binary
3839
geometry: typing.Callable = type_utils.text_recv
40+
interval: typing.Callable = type_utils.interval_recv_integer
3941

4042

4143
test_data: typing.Dict[Datatypes, typing.List[typing.Tuple]] = {
@@ -329,6 +331,16 @@ class Datatypes(Enum):
329331
"0103000020E61000000100000005000000000000000000000000000000000000000000000000000000000000000000F03F000000000000F03F000000000000F03F000000000000F03F000000000000000000000000000000000000000000000000",
330332
),
331333
],
334+
Datatypes.interval: [
335+
(b"\x00\x01\x00\x00\x00\x0c\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01", 6, 12, Interval(months=1)),
336+
(
337+
b"\x00\x01\x00\x00\x00\x0c\x00\x00\x00d\x954\xe0\x00\x00\x00\x00\x01",
338+
6,
339+
12,
340+
Interval(months=1, microseconds=432000000000),
341+
),
342+
(b"\x00\x01\x00\x00\x00\x0c\x00\x00\x00d\x954\xe0\x00\x00\x00\x00\x00", 6, 12, timedelta(days=5)),
343+
],
332344
}
333345

334346

0 commit comments

Comments
 (0)