]> git.ipfire.org Git - thirdparty/sqlalchemy/sqlalchemy.git/commitdiff
organize datetime types for correct impls
authorMike Bayer <mike_mp@zzzcomputing.com>
Fri, 1 Oct 2021 16:53:40 +0000 (12:53 -0400)
committerMike Bayer <mike_mp@zzzcomputing.com>
Fri, 1 Oct 2021 21:15:10 +0000 (17:15 -0400)
Fixed bug in SQL Server ``DATETIMEOFFSET`` where the ODBC implementation
would not generate the correct DDL, for cases where the type were converted
using the ``dialect.type_descriptor()`` method, the usage of which is
illustrated in some documented examples for :class:`.TypeDecorator`, though
not necessary for most datatypes. Regression was introduced by
:ticket:`6366`. As part of this change, the full list of SQL Server date
types have been amended to return a "dialect impl" that generates the same
DDL name as the supertype.

Fixes: #7129
Change-Id: I7d9bea54c0c38e16d1a6ad978cca996006a1b624

doc/build/changelog/unreleased_14/7129.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/7129.rst b/doc/build/changelog/unreleased_14/7129.rst
new file mode 100644 (file)
index 0000000..aa19725
--- /dev/null
@@ -0,0 +1,12 @@
+.. change::
+    :tags: bug, mssql, regression
+    :tickets: 7129
+
+    Fixed bug in SQL Server ``DATETIMEOFFSET`` where the ODBC implementation
+    would not generate the correct DDL, for cases where the type were converted
+    using the ``dialect.type_descriptor()`` method, the usage of which is
+    illustrated in some documented examples for :class:`.TypeDecorator`, though
+    not necessary for most datatypes. Regression was introduced by
+    :ticket:`6366`. As part of this change, the full list of SQL Server date
+    types have been amended to return a "dialect impl" that generates the same
+    DDL name as the supertype.
index 7946633eb5861a20e275a53d4f2d0f62498192d3..0dff4213999e35a039daacd48f327f5a28cee2d2 100644 (file)
@@ -1081,6 +1081,10 @@ class TIME(sqltypes.TIME):
 _MSTime = TIME
 
 
+class _BASETIMEIMPL(TIME):
+    __visit_name__ = "_BASETIMEIMPL"
+
+
 class _DateTimeBase(object):
     def bind_processor(self, dialect):
         def process(value):
@@ -1481,6 +1485,9 @@ class MSTypeCompiler(compiler.GenericTypeCompiler):
         else:
             return self.visit_DATE(type_, **kw)
 
+    def visit__BASETIMEIMPL(self, type_, **kw):
+        return self.visit_time(type_, **kw)
+
     def visit_time(self, type_, **kw):
         if self.dialect.server_version_info < MS_2008_VERSION:
             return self.visit_DATETIME(type_, **kw)
@@ -2608,9 +2615,13 @@ class MSDialect(default.DefaultDialect):
         sqltypes.JSON: JSON,
         sqltypes.JSON.JSONIndexType: JSONIndexType,
         sqltypes.JSON.JSONPathType: JSONPathType,
-        sqltypes.Time: TIME,
+        sqltypes.Time: _BASETIMEIMPL,
         sqltypes.Unicode: _MSUnicode,
         sqltypes.UnicodeText: _MSUnicodeText,
+        DATETIMEOFFSET: DATETIMEOFFSET,
+        DATETIME2: DATETIME2,
+        SMALLDATETIME: SMALLDATETIME,
+        DATETIME: DATETIME,
     }
 
     engine_config_types = default.DefaultDialect.engine_config_types.union(
index a27a3c5d02376df12bcb07ba4fb0931cc5e19f81..5a6e9a12bd464f7082f50a2edc6b2a81049da19f 100644 (file)
@@ -350,7 +350,7 @@ class _ms_binary_pyodbc(object):
         return process
 
 
-class _ODBCDateTime(sqltypes.DateTime):
+class _ODBCDateTimeBindProcessor(object):
     """Add bind processors to handle datetimeoffset behaviors"""
 
     has_tz = False
@@ -381,7 +381,11 @@ class _ODBCDateTime(sqltypes.DateTime):
         return process
 
 
-class _ODBCDATETIMEOFFSET(_ODBCDateTime):
+class _ODBCDateTime(_ODBCDateTimeBindProcessor, sqltypes.DateTime):
+    pass
+
+
+class _ODBCDATETIMEOFFSET(_ODBCDateTimeBindProcessor, DATETIMEOFFSET):
     has_tz = True
 
 
index c46cf825849f70b242da60671c183ed924d080a6..7e238dd81960ec32289839ed09e7fd589eee27d7 100644 (file)
@@ -395,68 +395,83 @@ class TypeDDLTest(fixtures.TestBase):
             )
             self.assert_(repr(col))
 
-    def test_dates(self):
+    @testing.combinations(
+        # column type, args, kwargs, expected ddl
+        (mssql.MSDateTime, [], {}, "DATETIME", None),
+        (types.DATE, [], {}, "DATE", None),
+        (types.Date, [], {}, "DATE", None),
+        (types.Date, [], {}, "DATETIME", MS_2005_VERSION),
+        (mssql.MSDate, [], {}, "DATE", None),
+        (mssql.MSDate, [], {}, "DATETIME", MS_2005_VERSION),
+        (types.TIME, [], {}, "TIME", None),
+        (types.Time, [], {}, "TIME", None),
+        (mssql.MSTime, [], {}, "TIME", None),
+        (mssql.MSTime, [1], {}, "TIME(1)", None),
+        (types.Time, [], {}, "DATETIME", MS_2005_VERSION),
+        (mssql.MSTime, [], {}, "TIME", None),
+        (mssql.MSSmallDateTime, [], {}, "SMALLDATETIME", None),
+        (mssql.MSDateTimeOffset, [], {}, "DATETIMEOFFSET", None),
+        (mssql.MSDateTimeOffset, [1], {}, "DATETIMEOFFSET(1)", None),
+        (mssql.MSDateTime2, [], {}, "DATETIME2", None),
+        (mssql.MSDateTime2, [0], {}, "DATETIME2(0)", None),
+        (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),
+        argnames="type_, args, kw, res, server_version",
+    )
+    @testing.combinations((True,), (False,), argnames="use_type_descriptor")
+    @testing.combinations(
+        ("base",), ("pyodbc",), ("pymssql",), argnames="driver"
+    )
+    def test_dates(
+        self, type_, args, kw, res, server_version, use_type_descriptor, driver
+    ):
         "Exercise type specification for date types."
 
-        columns = [
-            # column type, args, kwargs, expected ddl
-            (mssql.MSDateTime, [], {}, "DATETIME", None),
-            (types.DATE, [], {}, "DATE", None),
-            (types.Date, [], {}, "DATE", None),
-            (types.Date, [], {}, "DATETIME", MS_2005_VERSION),
-            (mssql.MSDate, [], {}, "DATE", None),
-            (mssql.MSDate, [], {}, "DATETIME", MS_2005_VERSION),
-            (types.TIME, [], {}, "TIME", None),
-            (types.Time, [], {}, "TIME", None),
-            (mssql.MSTime, [], {}, "TIME", None),
-            (mssql.MSTime, [1], {}, "TIME(1)", None),
-            (types.Time, [], {}, "DATETIME", MS_2005_VERSION),
-            (mssql.MSTime, [], {}, "TIME", None),
-            (mssql.MSSmallDateTime, [], {}, "SMALLDATETIME", None),
-            (mssql.MSDateTimeOffset, [], {}, "DATETIMEOFFSET", None),
-            (mssql.MSDateTimeOffset, [1], {}, "DATETIMEOFFSET(1)", None),
-            (mssql.MSDateTime2, [], {}, "DATETIME2", None),
-            (mssql.MSDateTime2, [0], {}, "DATETIME2(0)", None),
-            (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),
-        ]
+        if driver == "base":
+            from sqlalchemy.dialects.mssql import base
+
+            dialect = base.MSDialect()
+        elif driver == "pyodbc":
+            from sqlalchemy.dialects.mssql import pyodbc
+
+            dialect = pyodbc.dialect()
+        elif driver == "pymssql":
+            from sqlalchemy.dialects.mssql import pymssql
+
+            dialect = pymssql.dialect()
+        else:
+            assert False
+
+        if server_version:
+            dialect.server_version_info = server_version
+        else:
+            dialect.server_version_info = MS_2008_VERSION
 
         metadata = MetaData()
-        table_args = ["test_mssql_dates", metadata]
-        for index, spec in enumerate(columns):
-            type_, args, kw, res, server_version = spec
-            table_args.append(
-                Column("c%s" % index, type_(*args, **kw), nullable=None)
-            )
 
-        date_table = Table(*table_args)
-        dialect = mssql.dialect()
-        dialect.server_version_info = MS_2008_VERSION
-        ms_2005_dialect = mssql.dialect()
-        ms_2005_dialect.server_version_info = MS_2005_VERSION
+        typ = type_(*args, **kw)
+
+        if use_type_descriptor:
+            typ = dialect.type_descriptor(typ)
+
+        col = Column("date_c", typ, nullable=None)
+
+        date_table = Table("test_mssql_dates", metadata, col)
         gen = dialect.ddl_compiler(dialect, schema.CreateTable(date_table))
-        gen2005 = ms_2005_dialect.ddl_compiler(
-            ms_2005_dialect, schema.CreateTable(date_table)
-        )
 
-        for col in date_table.c:
-            index = int(col.name[1:])
-            server_version = columns[index][4]
-            if not server_version:
-                testing.eq_(
-                    gen.get_column_specification(col),
-                    "%s %s" % (col.name, columns[index][3]),
-                )
-            else:
-                testing.eq_(
-                    gen2005.get_column_specification(col),
-                    "%s %s" % (col.name, columns[index][3]),
-                )
+        testing.eq_(
+            gen.get_column_specification(col),
+            "%s %s"
+            % (
+                col.name,
+                res,
+            ),
+        )
 
-            self.assert_(repr(col))
+        self.assert_(repr(col))
 
     def test_large_type_deprecation(self):
         d1 = mssql.dialect(deprecate_large_types=True)