Skip to content

Commit 2aab6b5

Browse files
committed
feat(datatype, abstime): support abstime
1 parent 223759e commit 2aab6b5

File tree

3 files changed

+52
-0
lines changed

3 files changed

+52
-0
lines changed

redshift_connector/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@
4444
mask_secure_info_in_props,
4545
)
4646
from redshift_connector.utils.type_utils import (
47+
ABSTIME,
4748
BIGINT,
4849
BIGINTEGER,
4950
BOOLEAN,
@@ -406,6 +407,7 @@ def connect(
406407
"PGText",
407408
"PGVarchar",
408409
"__version__",
410+
"ABSTIME",
409411
"BIGINT",
410412
"BIGINTEGER",
411413
"BOOLEAN",

redshift_connector/utils/type_utils.py

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@
3232
)
3333

3434
ANY_ARRAY = 2277
35+
ABSTIME = 702
3536
BIGINT = 20
3637
BIGINT_ARRAY = 1016
3738
BOOLEAN = 16
@@ -209,6 +210,33 @@ def timestamp_recv_integer(data: bytes, offset: int, length: int) -> typing.Unio
209210
return Datetime.max
210211

211212

213+
def abstime_recv(data: bytes, offset: int, length: int) -> Datetime:
214+
"""
215+
Converts abstime values represented as integer, representing seconds, to Python datetime.Datetime using UTC.
216+
"""
217+
if length != 4:
218+
raise Exception("Malformed column value of type abstime received")
219+
220+
time: float = i_unpack(data, offset)[0]
221+
time *= 1000000 # Time in micro secs
222+
secs: float = time / 1000000
223+
nanos: int = int(time - secs * 1000000)
224+
if nanos < 0:
225+
secs -= 1
226+
nanos += 1000000
227+
228+
nanos *= 1000
229+
230+
millis: float = secs * float(1000)
231+
232+
total_secs: float = (millis / 1e3) + (nanos / 1e9)
233+
server_date: Datetime = Datetime.fromtimestamp(total_secs)
234+
# As of Version 3.0, times are no longer read and written using Greenwich Mean Time;
235+
# the input and output routines default to the local time zone.
236+
# Ref https://www.postgresql.org/docs/6.3/c0804.htm#abstime
237+
return server_date.astimezone(Timezone.utc)
238+
239+
212240
# data is 64-bit integer representing microseconds since 2000-01-01
213241
def timestamp_send_integer(v: Datetime) -> bytes:
214242
return q_pack(int((timegm(v.timetuple()) - EPOCH_SECONDS) * 1e6) + v.microsecond)
@@ -588,6 +616,7 @@ def varbytehex_recv(data: bytes, idx: int, length: int) -> str:
588616
pg_types: typing.DefaultDict[int, typing.Tuple[int, typing.Callable]] = defaultdict(
589617
lambda: (FC_TEXT, text_recv),
590618
{
619+
ABSTIME: (FC_BINARY, abstime_recv), # abstime
591620
BOOLEAN: (FC_BINARY, bool_recv), # boolean
592621
# 17: (FC_BINARY, bytea_recv), # bytea
593622
NAME: (FC_BINARY, text_recv), # name type

test/integration/datatype/test_datatypes.py

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22
import datetime
33
import os
44
import typing
5+
from datetime import datetime as Datetime
6+
from datetime import timezone
57
from math import isclose
68
from test.integration.datatype._generate_test_datatype_tables import ( # type: ignore
79
DATATYPES_WITH_MS,
@@ -160,3 +162,22 @@ def test_redshift_timetz_client_tz_applied(db_kwargs, _input, client_protocol):
160162
results: typing.Tuple = cursor.fetchone()
161163
assert len(results) == 1
162164
assert results[0] == t_time
165+
166+
167+
abstime_vals: typing.List[typing.Tuple[str, Datetime]] = [
168+
("infinity", Datetime(year=2038, month=1, day=19, hour=3, minute=14, second=4, tzinfo=timezone.utc)),
169+
("-infinity", Datetime(year=1901, month=12, day=13, hour=20, minute=45, second=52, tzinfo=timezone.utc)),
170+
("2022-06-10", Datetime(year=2022, month=6, day=10, tzinfo=timezone.utc)),
171+
]
172+
173+
174+
@pytest.mark.parametrize("client_protocol", ClientProtocolVersion.list())
175+
@pytest.mark.parametrize("_input", abstime_vals)
176+
def test_abstime(db_kwargs, _input, client_protocol):
177+
insert_val, exp_val = _input
178+
179+
with redshift_connector.connect(**db_kwargs) as conn:
180+
with conn.cursor() as cursor:
181+
cursor.execute("select '{}'::abstime".format(insert_val))
182+
res = cursor.fetchone()
183+
assert res[0] == exp_val

0 commit comments

Comments
 (0)