]> git.ipfire.org Git - thirdparty/sqlalchemy/sqlalchemy.git/commitdiff
Pass all datetime values to pyodbc for timezone-naive column
authorMike Bayer <mike_mp@zzzcomputing.com>
Mon, 26 Apr 2021 23:17:06 +0000 (19:17 -0400)
committerMike Bayer <mike_mp@zzzcomputing.com>
Tue, 27 Apr 2021 00:49:38 +0000 (20:49 -0400)
Fixed regression caused by :ticket:`6306` which added support for
``DateTime(timezone=True)``, where the previous behavior of the pyodbc
driver of implicitly dropping the tzinfo from a timezone-aware date when
INSERTing into a timezone-naive DATETIME column were lost, leading to a SQL
Server error when inserting timezone-aware datetime objects into
timezone-native database columns.

Fixes: #6366
Change-Id: Id7821de13d75ede27f2165b37277a7223468dfa4

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

index 4482058c1ef453e38531d3d5e575aaa78b93f3f9..5bef3856a357d3f9bacc6c1c70b890a50bbc221c 100644 (file)
@@ -63,7 +63,7 @@ This document details individual issue-level changes made throughout
 
 
     .. change::
-        :tags: usecase, schema, mssql
+        :tags: usecase, mssql
         :tickets: 6306
 
         The :paramref:`_types.DateTime.timezone` parameter when set to ``True``
diff --git a/doc/build/changelog/unreleased_14/6366.rst b/doc/build/changelog/unreleased_14/6366.rst
new file mode 100644 (file)
index 0000000..1a26d73
--- /dev/null
@@ -0,0 +1,10 @@
+.. change::
+    :tags: bug, mssql, regression
+    :tickets: 6366
+
+    Fixed regression caused by :ticket:`6306` which added support for
+    ``DateTime(timezone=True)``, where the previous behavior of the pyodbc
+    driver of implicitly dropping the tzinfo from a timezone-aware date when
+    INSERTing into a timezone-naive DATETIME column were lost, leading to a SQL
+    Server error when inserting timezone-aware datetime objects into
+    timezone-native database columns.
index 424adadfcdcbefbd96847142f06ebebc5bd9c094..95b2bb48eb526f06e290fefb5dd6bc81c9d4b93b 100644 (file)
@@ -353,6 +353,8 @@ class _ms_binary_pyodbc(object):
 class _ODBCDateTime(sqltypes.DateTime):
     """Add bind processors to handle datetimeoffset behaviors"""
 
+    has_tz = False
+
     def bind_processor(self, dialect):
         def process(value):
             if value is None:
@@ -360,7 +362,7 @@ class _ODBCDateTime(sqltypes.DateTime):
             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:
+            elif not value.tzinfo or (not self.timezone and not self.has_tz):
                 # for DateTime(timezone=False)
                 return value
             else:
@@ -379,6 +381,10 @@ class _ODBCDateTime(sqltypes.DateTime):
         return process
 
 
+class _ODBCDATETIMEOFFSET(_ODBCDateTime):
+    has_tz = True
+
+
 class _VARBINARY_pyodbc(_ms_binary_pyodbc, VARBINARY):
     pass
 
@@ -453,7 +459,7 @@ class MSDialect_pyodbc(PyODBCConnector, MSDialect):
             BINARY: _BINARY_pyodbc,
             # support DateTime(timezone=True)
             sqltypes.DateTime: _ODBCDateTime,
-            DATETIMEOFFSET: _ODBCDateTime,
+            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
index 7fb2f05ac038be17151469598e0424f71f7cc9ca..4e78ec1220108a950116c4a9179cace9bdbe0d8a 100644 (file)
@@ -805,6 +805,40 @@ class TypeRoundTripTest(
             (d1, d2, t1, d2.time(), d3),
         )
 
+    @testing.combinations(
+        (
+            datetime.datetime(
+                2007,
+                10,
+                30,
+                11,
+                2,
+                32,
+                tzinfo=util.timezone(datetime.timedelta(hours=-5)),
+            ),
+        ),
+        (datetime.datetime(2007, 10, 30, 11, 2, 32)),
+        argnames="date",
+    )
+    def test_tz_present_or_non_in_dates(self, date_fixture, connection, date):
+        t, (d1, t1, d2, d3) = date_fixture
+        connection.execute(
+            t.insert(),
+            dict(
+                adatetime=date,
+                adatetimewithtimezone=date,
+            ),
+        )
+
+        row = connection.execute(
+            select(t.c.adatetime, t.c.adatetimewithtimezone)
+        ).first()
+
+        if not date.tzinfo:
+            eq_(row, (date, date.replace(tzinfo=util.timezone.utc)))
+        else:
+            eq_(row, (date.replace(tzinfo=None), date))
+
     @testing.metadata_fixture()
     def datetimeoffset_fixture(self, metadata):
         t = Table(