Skip to content

Commit 8a75db9

Browse files
authored
Merge pull request #346 from baldwicc/datetimeoffset-patch
fix: adds pyodbc output converter for DATETIMEOFFSET data types
2 parents 495a284 + ee953e8 commit 8a75db9

File tree

2 files changed

+93
-0
lines changed

2 files changed

+93
-0
lines changed

dbt/adapters/sqlserver/sql_server_connection_manager.py

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import datetime as dt
12
import struct
23
import time
34
from contextlib import contextmanager
@@ -233,6 +234,40 @@ def bool_to_connection_string_arg(key: str, value: bool) -> str:
233234
return f'{key}={"Yes" if value else "No"}'
234235

235236

237+
def byte_array_to_datetime(value: bytes) -> dt.datetime:
238+
"""
239+
Converts a DATETIMEOFFSET byte array to a timezone-aware datetime object
240+
241+
Parameters
242+
----------
243+
value : buffer
244+
A binary value conforming to SQL_SS_TIMESTAMPOFFSET_STRUCT
245+
246+
Returns
247+
-------
248+
out : datetime
249+
250+
Source
251+
------
252+
SQL_SS_TIMESTAMPOFFSET datatype and SQL_SS_TIMESTAMPOFFSET_STRUCT layout:
253+
https://learn.microsoft.com/sql/relational-databases/native-client-odbc-date-time/data-type-support-for-odbc-date-and-time-improvements
254+
"""
255+
# unpack 20 bytes of data into a tuple of 9 values
256+
tup = struct.unpack("<6hI2h", value)
257+
258+
# construct a datetime object
259+
return dt.datetime(
260+
year=tup[0],
261+
month=tup[1],
262+
day=tup[2],
263+
hour=tup[3],
264+
minute=tup[4],
265+
second=tup[5],
266+
microsecond=tup[6] // 1000, # https://bugs.python.org/issue15443
267+
tzinfo=dt.timezone(dt.timedelta(hours=tup[7], minutes=tup[8])),
268+
)
269+
270+
236271
class SQLServerConnectionManager(SQLConnectionManager):
237272
TYPE = "sqlserver"
238273

@@ -394,6 +429,10 @@ def add_query(
394429
else:
395430
cursor.execute(sql, bindings)
396431

432+
# convert DATETIMEOFFSET binary structures to datetime ojbects
433+
# https://github.com/mkleehammer/pyodbc/issues/134#issuecomment-281739794
434+
connection.handle.add_output_converter(-155, byte_array_to_datetime)
435+
397436
logger.debug(
398437
"SQL status: {} in {:0.2f} seconds".format(
399438
self.get_response(cursor), (time.time() - pre)

tests/unit/adapters/sqlserver/test_sql_server_connection_manager.py

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77

88
from dbt.adapters.sqlserver.sql_server_connection_manager import (
99
bool_to_connection_string_arg,
10+
byte_array_to_datetime,
1011
get_pyodbc_attrs_before,
1112
)
1213
from dbt.adapters.sqlserver.sql_server_credentials import SQLServerCredentials
@@ -76,3 +77,56 @@ def test_get_pyodbc_attrs_before_contains_access_token_key_for_cli_authenticatio
7677
)
7778
def test_bool_to_connection_string_arg(key: str, value: bool, expected: str) -> None:
7879
assert bool_to_connection_string_arg(key, value) == expected
80+
81+
82+
@pytest.mark.parametrize(
83+
"value, expected_datetime, expected_str",
84+
[
85+
(
86+
bytes(
87+
[
88+
0xE6,
89+
0x07, # 2022 year unsigned short
90+
0x0C,
91+
0x00, # 12 month unsigned short
92+
0x11,
93+
0x00, # 17 day unsigned short
94+
0x11,
95+
0x00, # 17 hour unsigned short
96+
0x34,
97+
0x00, # 52 minute unsigned short
98+
0x12,
99+
0x00, # 18 second unsigned short
100+
0xBC,
101+
0xCC,
102+
0x5B,
103+
0x07, # 123456700 10⁻⁷ second unsigned long
104+
0xFE,
105+
0xFF, # -2 offset hour signed short
106+
0xE2,
107+
0xFF, # -30 offset minute signed short
108+
]
109+
),
110+
dt.datetime(
111+
year=2022,
112+
month=12,
113+
day=17,
114+
hour=17,
115+
minute=52,
116+
second=18,
117+
microsecond=123456700 // 1000, # 10⁻⁶ second
118+
tzinfo=dt.timezone(dt.timedelta(hours=-2, minutes=-30)),
119+
),
120+
"2022-12-17 17:52:18.123456-02:30",
121+
)
122+
],
123+
)
124+
def test_byte_array_to_datetime(
125+
value: bytes, expected_datetime: dt.datetime, expected_str: str
126+
) -> None:
127+
"""
128+
Assert SQL_SS_TIMESTAMPOFFSET_STRUCT bytes are converted to datetime and str
129+
https://learn.microsoft.com/sql/relational-databases/native-client-odbc-date-time/data-type-support-for-odbc-date-and-time-improvements#sql_ss_timestampoffset_struct
130+
"""
131+
assert byte_array_to_datetime(value) == expected_datetime
132+
assert str(byte_array_to_datetime(value)) == expected_str

0 commit comments

Comments
 (0)