From: Gord Thompson Date: Sun, 18 Apr 2021 18:15:48 +0000 (-0600) Subject: Add DateTime(timezone=True) support to mssql X-Git-Tag: rel_1_4_10~4^2 X-Git-Url: http://git.ipfire.org/?a=commitdiff_plain;h=0c801c8a6617523796f304268d0417a59469a37c;p=thirdparty%2Fsqlalchemy%2Fsqlalchemy.git Add DateTime(timezone=True) support to mssql 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 --- diff --git a/doc/build/changelog/unreleased_14/6306.rst b/doc/build/changelog/unreleased_14/6306.rst new file mode 100644 index 0000000000..a862ca8be5 --- /dev/null +++ b/doc/build/changelog/unreleased_14/6306.rst @@ -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. diff --git a/lib/sqlalchemy/dialects/mssql/base.py b/lib/sqlalchemy/dialects/mssql/base.py index e265487504..8c2abc8e7b 100644 --- a/lib/sqlalchemy/dialects/mssql/base.py +++ b/lib/sqlalchemy/dialects/mssql/base.py @@ -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: diff --git a/lib/sqlalchemy/dialects/mssql/pyodbc.py b/lib/sqlalchemy/dialects/mssql/pyodbc.py index 99ab372289..424adadfcd 100644 --- a/lib/sqlalchemy/dialects/mssql/pyodbc.py +++ b/lib/sqlalchemy/dialects/mssql/pyodbc.py @@ -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 diff --git a/test/dialect/mssql/test_types.py b/test/dialect/mssql/test_types.py index 439828fe4d..7fb2f05ac0 100644 --- a/test/dialect/mssql/test_types.py +++ b/test/dialect/mssql/test_types.py @@ -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()