]> git.ipfire.org Git - thirdparty/sqlalchemy/sqlalchemy.git/commitdiff
Add DATETIMEOFFSET support for mssql+pyodbc
authorGord Thompson <gord@gordthompson.com>
Fri, 15 Nov 2019 21:53:50 +0000 (14:53 -0700)
committerGord Thompson <gord@gordthompson.com>
Fri, 15 Nov 2019 23:43:49 +0000 (16:43 -0700)
Fixes: #4983
.gitignore
lib/sqlalchemy/dialects/mssql/pyodbc.py
test/dialect/mssql/test_types.py

index 087085e50e130e9f97e945596026f9253d3b7a54..4c0ce515a5fd0fe8b0e52171c658df85ef378718 100644 (file)
@@ -32,3 +32,4 @@ test/test_schema.db
 *test_schema.db
 .idea
 /Pipfile*
+/db_idents.txt
index f12fb5eade1331554ca5c88e04e5fbd12509d6a9..de411d8f8f80fbb139106d452bd1109d68021ed5 100644 (file)
@@ -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
index f88eb928ca8ed753d8345738774f0dfe4563f55e..2b6d4b669e9ee418a029de9c3b781c45619cf4ae 100644 (file)
@@ -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.*")