From: Mike Bayer Date: Fri, 1 Oct 2021 16:53:40 +0000 (-0400) Subject: organize datetime types for correct impls X-Git-Tag: rel_1_4_26~48 X-Git-Url: http://git.ipfire.org/?a=commitdiff_plain;h=b230298234ea3a51c07538a37bee21d8ccb28303;p=thirdparty%2Fsqlalchemy%2Fsqlalchemy.git organize datetime types for correct impls 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 --- diff --git a/doc/build/changelog/unreleased_14/7129.rst b/doc/build/changelog/unreleased_14/7129.rst new file mode 100644 index 0000000000..aa19725529 --- /dev/null +++ b/doc/build/changelog/unreleased_14/7129.rst @@ -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. diff --git a/lib/sqlalchemy/dialects/mssql/base.py b/lib/sqlalchemy/dialects/mssql/base.py index 7946633eb5..0dff421399 100644 --- a/lib/sqlalchemy/dialects/mssql/base.py +++ b/lib/sqlalchemy/dialects/mssql/base.py @@ -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( diff --git a/lib/sqlalchemy/dialects/mssql/pyodbc.py b/lib/sqlalchemy/dialects/mssql/pyodbc.py index a27a3c5d02..5a6e9a12bd 100644 --- a/lib/sqlalchemy/dialects/mssql/pyodbc.py +++ b/lib/sqlalchemy/dialects/mssql/pyodbc.py @@ -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 diff --git a/test/dialect/mssql/test_types.py b/test/dialect/mssql/test_types.py index c46cf82584..7e238dd819 100644 --- a/test/dialect/mssql/test_types.py +++ b/test/dialect/mssql/test_types.py @@ -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)