1

I have a problem uploading a timezone aware datetime from python to the SQL server datatype datetimeoffset.

No matter which timezone upload to SQL Server, it always displays the offset in system time offset. It is not converting the actual datetime with only changing the offset.

    datetime = datetime.datetime.now(tz=pytz.timezone('UTC'))
    timeString = datetime.strftime("%Y-%m-%d %H:%M:%S%z")
    timeData = {'datetimeOFFSET':datetime, 'datetime':datetime, 'datetimeString':timeString}
    df = pd.DataFrame(data=timeData, index = np.array([1]))

The dataframe in python:

datetimeOFFSET                         datetime                           datetimeString
2019-12-30 10:29:07.913715+00:00       2019-12-30 10:29:07.913715+00:00   2019-12-30 10:29:07+0000

The result in sql server:

enter image description here

What I want is the datetimeOFFSET column to display the right offset (+00:00) and not the system offset (+01:00).

milanDD
  • 123
  • 1
  • 11

2 Answers2

2

Update, April 2021:

This remains a known issue with pandas .to_sql()


Original Answer (Warning: Includes outdated SQLAlchemy 1.3 usage patterns deprecated in version 1.4)

There have been some very recent (as in "this morning") improvements to datetimeoffset handling in SQLAlchemy. They will be included in the next release (probably 1.3.13), but in the meantime try installing from the latest source for the 1.3.x branch ...

pip install --upgrade git+https://github.com/sqlalchemy/sqlalchemy@rel_1_3

... and see if that works better for you.

Edit:

On further investigation it appears that the issue lies with to_sql. If the DataFrame contains a single row then the timezone offset is lost:

import datetime
from pprint import pprint

import sqlalchemy as sa

# ...

engine = sa.create_engine(connection_uri, fast_executemany=True)

# test environment
table_name = 'DateTimeOffset_Test'
engine.execute(sa.text(f"DROP TABLE IF EXISTS [{table_name}]"))
engine.execute(sa.text(f"CREATE TABLE [{table_name}] (id int primary key, dto datetimeoffset)"))

# test data
my_tz = datetime.timezone(datetime.timedelta(hours=-7))
dto_value = datetime.datetime(2020, 1, 1, 0, 0, 0, tzinfo=my_tz)
print(dto_value)  # 2020-01-01 00:00:00-07:00
#                                        ^

num_rows = 1
row_data = [(x, dto_value) for x in range(num_rows)]
df = pd.DataFrame(row_data, columns=['id', 'dto'])
print(df)
#    id                       dto
# 0   0 2020-01-01 00:00:00-07:00
#                            ^
df.to_sql(table_name, engine, if_exists='append', index=False)
result = engine.execute(sa.text(f"SELECT id, CAST(dto as varchar(50)) AS foo FROM [{table_name}]")).fetchall()
pprint(result)
# [(0, '2020-01-01 00:00:00.0000000 +00:00')]
#                                     ^ -- wrong

However, if the DataFrame contains more than one row then the datetimeoffset values are uploaded correctly:

# ...

num_rows = 2
row_data = [(x, dto_value) for x in range(num_rows)]
df = pd.DataFrame(row_data, columns=['id', 'dto'])
print(df)
#    id                       dto
# 0   0 2020-01-01 00:00:00-07:00
# 1   1 2020-01-01 00:00:00-07:00
#                            ^
df.to_sql(table_name, engine, if_exists='append', index=False)
result = engine.execute(sa.text(f"SELECT id, CAST(dto as varchar(50)) AS foo FROM [{table_name}]")).fetchall()
pprint(result)
# [(0, '2020-01-01 00:00:00.0000000 -07:00'), 
#  (1, '2020-01-01 00:00:00.0000000 -07:00')]
#                                     ^ -- correct

If you really feel strongly about this you might want to raise a pandas issue about it.

Gord Thompson
  • 116,920
  • 32
  • 215
  • 418
  • Using, for example, df.to_sql gives me the same error but in a different way. Instead of displaying the system time offset, it displays a UTC offset of +00:00 no matter the real offset of the data. df.to_sql("DateTimeOffset_Test", if_exists='append', con=engine, index=False) --> https://imgur.com/C9nyXej – milanDD Jan 02 '20 at 12:14
  • I appreciate the well-explained answer. But unfortunately, it does not solve my problem: [code and result](https://imgur.com/Ym8NSj4). This could due to an issue on my SQL Server. – milanDD Jan 03 '20 at 08:58
  • Can you run SQL Profiler to see just what is being sent to the server? When I run it I see `exec sp_prepare @p1 output,N'@P1 int,@P2 datetimeoffset',N'INSERT INTO [DateTimeOffset_Test] (id, dto) VALUES (@P1, @P2)',1` followed by `exec sp_execute 2,0,'2020-01-01 00:00:00 -07:00'` and `exec sp_execute 2,1,'2020-01-01 00:00:00 -07:00'` – Gord Thompson Jan 04 '20 at 00:11
  • You are right! It is being sent wrong to the server, but I do not understand why: `exec sp_describe_undeclared_parameters N'INSERT INTO [DateTimeOffset_Test] ([datetimeOFFSET], datetime, [datetimeString]) VALUES (@P1, @P2, @P3)'` followed by `exec( sp_execute 2,'2020-01-06 10:16:31.2300000 +01:00','2020-01-06 10:16:31.2310000',N'2020-01-06 10:16:31+0000')`. As you can see when the datetime is sent as string (third column), it is sent correct, but not when it is tz aware datetime (first column). – milanDD Jan 06 '20 at 09:20
  • btw. the +01:00 added is my local timeoffset – milanDD Jan 06 '20 at 09:44
  • What version of pyodbc are you using? Are you specifying `fast_executemany=True` in your `create_engine` call? Have you tested with a DataFrame that contains more than one row? – Gord Thompson Jan 06 '20 at 11:07
  • I have found the issue, it is `fast_executemany=True`. The time offset error appears when it is set to True when I set it to false the data is loaded correctly. But this can not be the solution because, without fast_executemany set to True, the upload is way too slow. – milanDD Jan 06 '20 at 12:04
  • Are you using `DRIVER=ODBC Driver 17 for SQL Server` ? – Gord Thompson Jan 06 '20 at 13:26
  • yes I am, `engine = create_engine("mssql+pyodbc://myDatabaseInformation?driver=ODBC+Driver+17+for+SQL+Server", pool_pre_ping=True, fast_executemany=True)` – milanDD Jan 06 '20 at 13:31
  • I found that it works with fast_executemany when sending the DateTime with an offset as a plain string instead of DateTime dtype. – milanDD Jan 07 '20 at 09:26
0

Did you try this options ?

Time zone offset aware and preservation     No
Daylight saving aware                       No