I am adding output converters to pyodbc
connection objects to handle date types returned from SQL Server. I was able to unpack a datetime.time
struct with:
tuple = struct.unpack("HHHI", dateObj)
which works well. I cannot figure out the secret sauce for the datetime.datetime
object, though, which according to the pyodbc docs is a TIMESTAMP_STRUCT, defined here:
typedef struct tagTIMESTAMP_STRUCT
{
SQLSMALLINT year;
SQLUSMALLINT month;
SQLUSMALLINT day;
SQLUSMALLINT hour;
SQLUSMALLINT minute;
SQLUSMALLINT second;
SQLUINTEGER fraction;
} TIMESTAMP_STRUCT;
A datum from that column in the db is 2018-01-11 11:50:16.000
, and without the add_output_convert
trap pyodbc returns:
TypeError: datetime.datetime(2018, 1, 11, 11, 50, 16) is not JSON serializable
Looks like pyodbc silently drops the fraction, which is fine. Shouldn't the unpack()
format be one of:
tuple = struct.unpack("hHHHHHI", dateObj) # with missing fraction
tuple = struct.unpack("hHHHHH", dateObj)
? The latter simply returns:
error: unpack requires a string argument of length 12
For the record, according to sys.getsizeof
dateObj is 41 bytes. Any suggestions for the format? This is Windows 10 64-bit, as well as Linux 64-bit.
You seem to have been chasing down some false leads. The SQL Server ODBC driver does not return 41 bytes of data for a DATETIME value, it only returns 8 bytes. (sys.getsizeof
returns a value of 41 because it includes "overhead" related to garbage collection.) And there's no way that those 8 bytes represent a TIMESTAMP_STRUCT, so it must be something else.
Starting with a basic test ...
import pyodbc
def datetime_as_string(raw_bytes):
return raw_bytes
cnxn = pyodbc.connect('DSN=SQLmyDb;', autocommit=True)
cnxn.add_output_converter(pyodbc.SQL_TYPE_TIMESTAMP, datetime_as_string)
crsr = cnxn.cursor()
test_value = '2018-01-11 11:50:16'
rtn = crsr.execute("SELECT CAST(? AS DATETIME)", test_value).fetchval()
print(repr(rtn))
crsr.close()
cnxn.close()
... I saw that your test value is represented by 'e\xa8\x00\x00\xa0\x14\xc3\x00'
. A bit of time with the hex converter in Windows Calculator showed me that the contents were not immediately obvious. Playing a hunch, I tried test_value = '1900-01-01 00:00:00'
and that returned '\x00\x00\x00\x00\x00\x00\x00\x00'
so at least I had a place to start (the "epoch" for SQL Server DATETIME values).
test_value = '1901-01-01 00:00:00'
(1 year after the epoch) returned 'm\x01\x00\x00\x00\x00\x00\x00'
, 'm'
is 0x6d
, and 0x016d
is 365, so that was encouraging.
test_value = '1900-01-01 00:00:01'
(1 second after the epoch) returned '\x00\x00\x00\x00,\x01\x00\x00'
, ','
is 0x2c
, and 0x012c
is 300.
test_value = '1900-01-01 00:00:02'
(2 seconds after the epoch) returned '\x00\x00\x00\x00X\x02\x00\x00'
, 'X'
is 0x58
, and 0x0258
is 600.
So the SQL Server ODBC driver is returning two 4-byte signed integers, the first being the offset from the epoch in whole days, and the second being the partial day in increments of 1/300 seconds.
Therefore I changed my Output Converter function to
def datetime_as_string(raw_bytes):
tup = struct.unpack("<2l", raw_bytes)
days_since_1900 = tup[0]
partial_day = round(tup[1] / 300.0, 3)
date_time = datetime(1900, 1, 1) + timedelta(days=days_since_1900) + timedelta(seconds=partial_day)
return date_time.strftime('%Y-%m-%d %H:%M:%S.%f')[:23]
and that seemed to do the trick.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With