From: Gord Thompson Date: Fri, 15 Nov 2019 21:53:50 +0000 (-0700) Subject: Add DATETIMEOFFSET support for mssql+pyodbc X-Git-Url: http://git.ipfire.org/cgi-bin/gitweb.cgi?a=commitdiff_plain;h=c0a42fbf770762026c7a4c45e97c03b07afa2aaa;p=thirdparty%2Fsqlalchemy%2Fsqlalchemy.git Add DATETIMEOFFSET support for mssql+pyodbc Fixes: #4983 --- diff --git a/.gitignore b/.gitignore index 087085e50e..4c0ce515a5 100644 --- a/.gitignore +++ b/.gitignore @@ -32,3 +32,4 @@ test/test_schema.db *test_schema.db .idea /Pipfile* +/db_idents.txt diff --git a/lib/sqlalchemy/dialects/mssql/pyodbc.py b/lib/sqlalchemy/dialects/mssql/pyodbc.py index f12fb5eade..de411d8f8f 100644 --- a/lib/sqlalchemy/dialects/mssql/pyodbc.py +++ b/lib/sqlalchemy/dialects/mssql/pyodbc.py @@ -118,10 +118,13 @@ in order to use this flag:: """ # noqa +from datetime import datetime, timezone, timedelta import decimal import re +import struct from .base import BINARY +from .base import DATETIMEOFFSET from .base import MSDialect from .base import MSExecutionContext from .base import VARBINARY @@ -226,6 +229,18 @@ class _ms_binary_pyodbc(object): return process +class _ODBCDateTimeOffset(DATETIMEOFFSET): + + def bind_processor(self, dialect): + def process(value): + """Convert to string format required by T-SQL.""" + dto_string = value.strftime("%Y-%m-%d %H:%M:%S %z") + # offset needs a colon, e.g., -0700 -> -07:00 + return dto_string[:23] + ":" + dto_string[23:] + + return process + + class _VARBINARY_pyodbc(_ms_binary_pyodbc, VARBINARY): pass @@ -294,6 +309,7 @@ class MSDialect_pyodbc(PyODBCConnector, MSDialect): sqltypes.Numeric: _MSNumeric_pyodbc, sqltypes.Float: _MSFloat_pyodbc, BINARY: _BINARY_pyodbc, + DATETIMEOFFSET: _ODBCDateTimeOffset, # SQL Server dialect has a VARBINARY that is just to support # "deprecate_large_types" w/ VARBINARY(max), but also we must # handle the usual SQL standard VARBINARY @@ -345,6 +361,25 @@ class MSDialect_pyodbc(PyODBCConnector, MSDialect): pass return tuple(version) + def on_connect(self): + super_ = super(MSDialect_pyodbc, self).on_connect() + + def on_connect(conn): + if super_ is not None: + super_(conn) + + # output converter function for datetimeoffset + def _handle_datetimeoffset(dto_value): + # ref: https://github.com/mkleehammer/pyodbc/issues/134#issuecomment-281739794 + tup = struct.unpack("<6hI2h", dto_value) # e.g., (2017, 3, 16, 10, 35, 18, 0, -6, 0) + return datetime(tup[0], tup[1], tup[2], tup[3], tup[4], tup[5], tup[6] // 1000, + timezone(timedelta(hours=tup[7], minutes=tup[8]))) + + odbc_SQL_SS_TIMESTAMPOFFSET = -155 # as defined in SQLNCLI.h + conn.add_output_converter(odbc_SQL_SS_TIMESTAMPOFFSET, _handle_datetimeoffset) + + return on_connect + def do_executemany(self, cursor, statement, parameters, context=None): if self.fast_executemany: cursor.fast_executemany = True diff --git a/test/dialect/mssql/test_types.py b/test/dialect/mssql/test_types.py index f88eb928ca..2b6d4b669e 100644 --- a/test/dialect/mssql/test_types.py +++ b/test/dialect/mssql/test_types.py @@ -36,6 +36,7 @@ from sqlalchemy.databases import mssql from sqlalchemy.dialects.mssql import ROWVERSION from sqlalchemy.dialects.mssql import TIMESTAMP from sqlalchemy.dialects.mssql.base import _MSDate +from sqlalchemy.dialects.mssql.base import DATETIMEOFFSET from sqlalchemy.dialects.mssql.base import MS_2005_VERSION from sqlalchemy.dialects.mssql.base import MS_2008_VERSION from sqlalchemy.dialects.mssql.base import TIME @@ -721,34 +722,38 @@ class TypeRoundTripTest( Column("adate", Date), Column("atime", Time), Column("adatetime", DateTime), + Column("adatetimeoffset", DATETIMEOFFSET), ) metadata.create_all() d1 = datetime.date(2007, 10, 30) t1 = datetime.time(11, 2, 32) d2 = datetime.datetime(2007, 10, 30, 11, 2, 32) - t.insert().execute(adate=d1, adatetime=d2, atime=t1) + dto = datetime.datetime(2007, 10, 30, 11, 2, 32, 0, + datetime.timezone(datetime.timedelta(hours=1))) + t.insert().execute(adate=d1, adatetime=d2, atime=t1, adatetimeoffset=dto) # NOTE: this previously passed 'd2' for "adate" even though # "adate" is a date column; we asserted that it truncated w/o issue. # As of pyodbc 4.0.22, this is no longer accepted, was accepted # in 4.0.21. See also the new pyodbc assertions regarding numeric # precision. - t.insert().execute(adate=d1, adatetime=d2, atime=d2) + t.insert().execute(adate=d1, adatetime=d2, atime=d2, adatetimeoffset=dto) x = t.select().execute().fetchall()[0] self.assert_(x.adate.__class__ == datetime.date) self.assert_(x.atime.__class__ == datetime.time) self.assert_(x.adatetime.__class__ == datetime.datetime) + self.assert_(x.adatetimeoffset.__class__ == datetime.datetime) t.delete().execute() - t.insert().execute(adate=d1, adatetime=d2, atime=t1) + t.insert().execute(adate=d1, adatetime=d2, atime=t1, adatetimeoffset=dto) eq_( - select([t.c.adate, t.c.atime, t.c.adatetime], t.c.adate == d1) + select([t.c.adate, t.c.atime, t.c.adatetime, t.c.adatetimeoffset], t.c.adate == d1) .execute() .fetchall(), - [(d1, t1, d2)], + [(d1, t1, d2, dto)], ) @emits_warning_on("mssql+mxodbc", r".*does not have any indexes.*")