Skip to content

Commit ace8543

Browse files
baldwiccsdebruyn
authored andcommitted
add pyodbc output converter for DATETIMEOFFSET (-155)
1 parent 495a284 commit ace8543

File tree

2 files changed

+68
-0
lines changed

2 files changed

+68
-0
lines changed

dbt/adapters/sqlserver/sql_server_connection_manager.py

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import struct
22
import time
3+
import datetime as dt
34
from contextlib import contextmanager
45
from itertools import chain, repeat
56
from typing import Any, Callable, Dict, Mapping, Optional, Tuple
@@ -233,6 +234,39 @@ 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],
267+
tzinfo=dt.timezone(dt.timedelta(hours=tup[7], minutes=tup[8])),
268+
)
269+
236270
class SQLServerConnectionManager(SQLConnectionManager):
237271
TYPE = "sqlserver"
238272

@@ -394,6 +428,10 @@ def add_query(
394428
else:
395429
cursor.execute(sql, bindings)
396430

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

tests/unit/adapters/sqlserver/test_sql_server_connection_manager.py

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import datetime as dt
2+
import struct
23
import json
34
from unittest import mock
45

@@ -8,6 +9,7 @@
89
from dbt.adapters.sqlserver.sql_server_connection_manager import (
910
bool_to_connection_string_arg,
1011
get_pyodbc_attrs_before,
12+
byte_array_to_datetime
1113
)
1214
from dbt.adapters.sqlserver.sql_server_credentials import SQLServerCredentials
1315

@@ -76,3 +78,31 @@ def test_get_pyodbc_attrs_before_contains_access_token_key_for_cli_authenticatio
7678
)
7779
def test_bool_to_connection_string_arg(key: str, value: bool, expected: str) -> None:
7880
assert bool_to_connection_string_arg(key, value) == expected
81+
82+
@pytest.mark.parametrize(
83+
"value, expected", [
84+
(
85+
bytes([
86+
0xE5, 0x07, # year: 2022
87+
0x0C, 0x00, # month: 12
88+
0x11, 0x00, # day: 17
89+
0x16, 0x00, # hour: 22
90+
0x16, 0x00, # minute: 22
91+
0x12, 0x00, # second: 18
92+
0x40, 0xE2, 0x01, 0x00, # microsecond: 123456
93+
0x02, 0x00, 0x1E, 0x00 # tzinfo: +02:30
94+
]),
95+
dt.datetime(
96+
2021, 12, 17,
97+
22, 22,
98+
18, 123456,
99+
dt.timezone(dt.timedelta(hours=2, minutes=30))
100+
)
101+
)
102+
]
103+
)
104+
def test_byte_array_to_datetime(value: bytes, expected: dt.datetime) -> None:
105+
"""
106+
107+
"""
108+
assert byte_array_to_datetime(value) == expected

0 commit comments

Comments
 (0)