]> git.ipfire.org Git - thirdparty/sqlalchemy/sqlalchemy.git/commitdiff
Add DateTime(timezone=True) support to mssql
authorGord Thompson <gord@gordthompson.com>
Sun, 18 Apr 2021 18:15:48 +0000 (12:15 -0600)
committerMike Bayer <mike_mp@zzzcomputing.com>
Tue, 20 Apr 2021 17:13:15 +0000 (13:13 -0400)
The :paramref:`_types.DateTime.timezone` parameter when set to ``True``
will now make use of the ``DATETIMEOFFSET`` column type with SQL Server
when used to emit DDL, rather than ``DATETIME`` where the flag was silently
ignored.

Fixes: #6306
Change-Id: I4def8337046e8c190d424fa4a259ab24d5f9039e

doc/build/changelog/unreleased_14/6306.rst [new file with mode: 0644]
lib/sqlalchemy/dialects/mssql/base.py
lib/sqlalchemy/dialects/mssql/pyodbc.py
test/dialect/mssql/test_types.py

diff --git a/doc/build/changelog/unreleased_14/6306.rst b/doc/build/changelog/unreleased_14/6306.rst
new file mode 100644 (file)
index 0000000..a862ca8
--- /dev/null
@@ -0,0 +1,8 @@
+.. change::
+    :tags: usecase, schema, mssql
+    :tickets: 6306
+
+    The :paramref:`_types.DateTime.timezone` parameter when set to ``True``
+    will now make use of the ``DATETIMEOFFSET`` column type with SQL Server
+    when used to emit DDL, rather than ``DATETIME`` where the flag was silently
+    ignored.
index e26548750474c1878769f796f6dbf6a05cfaa5fb..8c2abc8e7b1c11e11a51841eecf466e5a1d370cd 100644 (file)
@@ -1398,12 +1398,6 @@ class MSTypeCompiler(compiler.GenericTypeCompiler):
     def visit_TINYINT(self, type_, **kw):
         return "TINYINT"
 
-    def visit_DATETIMEOFFSET(self, type_, **kw):
-        if type_.precision is not None:
-            return "DATETIMEOFFSET(%s)" % type_.precision
-        else:
-            return "DATETIMEOFFSET"
-
     def visit_TIME(self, type_, **kw):
         precision = getattr(type_, "precision", None)
         if precision is not None:
@@ -1417,6 +1411,19 @@ class MSTypeCompiler(compiler.GenericTypeCompiler):
     def visit_ROWVERSION(self, type_, **kw):
         return "ROWVERSION"
 
+    def visit_datetime(self, type_, **kw):
+        if type_.timezone:
+            return self.visit_DATETIMEOFFSET(type_, **kw)
+        else:
+            return self.visit_DATETIME(type_, **kw)
+
+    def visit_DATETIMEOFFSET(self, type_, **kw):
+        precision = getattr(type_, "precision", None)
+        if precision is not None:
+            return "DATETIMEOFFSET(%s)" % type_.precision
+        else:
+            return "DATETIMEOFFSET"
+
     def visit_DATETIME2(self, type_, **kw):
         precision = getattr(type_, "precision", None)
         if precision is not None:
index 99ab372289ce78e984cf5d43f44203c2ca287dd0..424adadfcdcbefbd96847142f06ebebc5bd9c094 100644 (file)
@@ -350,7 +350,9 @@ class _ms_binary_pyodbc(object):
         return process
 
 
-class _ODBCDateTimeOffset(DATETIMEOFFSET):
+class _ODBCDateTime(sqltypes.DateTime):
+    """Add bind processors to handle datetimeoffset behaviors"""
+
     def bind_processor(self, dialect):
         def process(value):
             if value is None:
@@ -358,7 +360,12 @@ class _ODBCDateTimeOffset(DATETIMEOFFSET):
             elif isinstance(value, util.string_types):
                 # if a string was passed directly, allow it through
                 return value
+            elif isinstance(value, datetime.datetime) and value.tzinfo is None:
+                # for DateTime(timezone=False)
+                return value
             else:
+                # for DATETIMEOFFSET or DateTime(timezone=True)
+                #
                 # Convert to string format required by T-SQL
                 dto_string = value.strftime("%Y-%m-%d %H:%M:%S.%f %z")
                 # offset needs a colon, e.g., -0700 -> -07:00
@@ -444,7 +451,9 @@ class MSDialect_pyodbc(PyODBCConnector, MSDialect):
             sqltypes.Numeric: _MSNumeric_pyodbc,
             sqltypes.Float: _MSFloat_pyodbc,
             BINARY: _BINARY_pyodbc,
-            DATETIMEOFFSET: _ODBCDateTimeOffset,
+            # support DateTime(timezone=True)
+            sqltypes.DateTime: _ODBCDateTime,
+            DATETIMEOFFSET: _ODBCDateTime,
             # 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
index 439828fe4d2db783ce85a3d2425e042cbab942af..7fb2f05ac038be17151469598e0424f71f7cc9ca 100644 (file)
@@ -419,6 +419,8 @@ class TypeDDLTest(fixtures.TestBase):
             (mssql.MSDateTime2, [1], {}, "DATETIME2(1)", None),
             (mssql.MSTime, [0], {}, "TIME(0)", None),
             (mssql.MSDateTimeOffset, [0], {}, "DATETIMEOFFSET(0)", None),
+            (types.DateTime, [], {"timezone": True}, "DATETIMEOFFSET", None),
+            (types.DateTime, [], {"timezone": False}, "DATETIME", None),
         ]
 
         metadata = MetaData()
@@ -760,23 +762,47 @@ class TypeRoundTripTest(
             Column("atime2", Time),
             Column("adatetime", DateTime),
             Column("adatetimeoffset", DATETIMEOFFSET),
+            Column("adatetimewithtimezone", DateTime(timezone=True)),
         )
 
         d1 = datetime.date(2007, 10, 30)
         t1 = datetime.time(11, 2, 32)
         d2 = datetime.datetime(2007, 10, 30, 11, 2, 32)
-        return t, (d1, t1, d2)
+        d3 = datetime.datetime(
+            2007,
+            10,
+            30,
+            11,
+            2,
+            32,
+            123456,
+            util.timezone(datetime.timedelta(hours=-5)),
+        )
+        return t, (d1, t1, d2, d3)
 
     def test_date_roundtrips(self, date_fixture, connection):
-        t, (d1, t1, d2) = date_fixture
+        t, (d1, t1, d2, d3) = date_fixture
         connection.execute(
-            t.insert(), dict(adate=d1, adatetime=d2, atime1=t1, atime2=d2)
+            t.insert(),
+            dict(
+                adate=d1,
+                adatetime=d2,
+                atime1=t1,
+                atime2=d2,
+                adatetimewithtimezone=d3,
+            ),
         )
 
         row = connection.execute(t.select()).first()
         eq_(
-            (row.adate, row.adatetime, row.atime1, row.atime2),
-            (d1, d2, t1, d2.time()),
+            (
+                row.adate,
+                row.adatetime,
+                row.atime1,
+                row.atime2,
+                row.adatetimewithtimezone,
+            ),
+            (d1, d2, t1, d2.time(), d3),
         )
 
     @testing.metadata_fixture()