Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Allow handling timezone-aware datetime values when inserting or updating #557

Merged
merged 3 commits into from
Jul 4, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion CHANGES.txt
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,11 @@ Unreleased
- SQLAlchemy DDL: Allow turning off column store using ``crate_columnstore=False``.
Thanks, @fetzerms.

- SQLAlchemy DDL: Allow setting ``server_default`` on columns to enable
server-generated defaults. Thanks, @JanLikar.

- Allow handling datetime values tagged with time zone info when inserting or updating.


2023/04/18 0.31.1
=================
Expand All @@ -16,7 +21,6 @@ Unreleased
SQLAlchemy 2.0 by adding the new ``insert_returning`` and ``update_returning`` flags
in the CrateDB dialect.

- SQLAlchemy DDL: Allow setting ``server_default`` on columns to enable server-generated defaults.

2023/03/30 0.31.0
=================
Expand Down
19 changes: 19 additions & 0 deletions docs/by-example/client.rst
Original file line number Diff line number Diff line change
Expand Up @@ -139,6 +139,25 @@ Refresh locations:

>>> cursor.execute("REFRESH TABLE locations")

Updating Data
=============

Values for ``TIMESTAMP`` columns can be obtained as a string literal, ``date``,
or ``datetime`` object. If it contains timezone information, it is converted to
UTC, and the timezone information is discarded.

>>> import datetime as dt
>>> timestamp_full = "2023-06-26T09:24:00.123+02:00"
>>> timestamp_date = "2023-06-26"
amotl marked this conversation as resolved.
Show resolved Hide resolved
>>> datetime_aware = dt.datetime.fromisoformat("2023-06-26T09:24:00.123+02:00")
>>> datetime_naive = dt.datetime.fromisoformat("2023-06-26T09:24:00.123")
>>> datetime_date = dt.date.fromisoformat("2023-06-26")
>>> cursor.execute("UPDATE locations SET date=? WHERE name='Cloverleaf'", (timestamp_full, ))
>>> cursor.execute("UPDATE locations SET date=? WHERE name='Cloverleaf'", (timestamp_date, ))
>>> cursor.execute("UPDATE locations SET date=? WHERE name='Cloverleaf'", (datetime_aware, ))
>>> cursor.execute("UPDATE locations SET date=? WHERE name='Cloverleaf'", (datetime_naive, ))
>>> cursor.execute("UPDATE locations SET date=? WHERE name='Cloverleaf'", (datetime_date, ))
Comment on lines +149 to +159
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If this includes date as well I guess the sentence above should mention that too.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks, added with 677bef3.


Selecting Data
==============

Expand Down
17 changes: 16 additions & 1 deletion docs/data-types.rst
Original file line number Diff line number Diff line change
Expand Up @@ -94,9 +94,21 @@ __ https://crate.io/docs/crate/reference/en/latest/general/ddl/data-types.html#c

.. NOTE::

The type that ``date`` and ``datetime`` objects are mapped depends on the
The type that ``date`` and ``datetime`` objects are mapped to, depends on the
CrateDB column type.

.. NOTE::

When using ``date`` or ``datetime`` objects with ``timezone`` information,
the value is implicitly converted to a `Unix time`_ (epoch) timestamp, i.e.
the number of seconds which have passed since 00:00:00 UTC on
Thursday, 1 January 1970.

This means, when inserting or updating records using timezone-aware Python
``date`` or ``datetime`` objects, timezone information will not be
preserved. If you need to store it, you will need to use a separate column.


.. _data-types-sqlalchemy:

SQLAlchemy
Expand Down Expand Up @@ -156,3 +168,6 @@ __ https://crate.io/docs/crate/reference/en/latest/general/ddl/data-types.html#o
__ https://crate.io/docs/crate/reference/en/latest/general/ddl/data-types.html#array
__ https://crate.io/docs/crate/reference/en/latest/general/ddl/data-types.html#geo-point
__ https://crate.io/docs/crate/reference/en/latest/general/ddl/data-types.html#geo-shape


.. _Unix time: https://en.wikipedia.org/wiki/Unix_time
10 changes: 7 additions & 3 deletions src/crate/client/http.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@
from urllib.parse import urlparse
from base64 import b64encode
from time import time
from datetime import datetime, date
from datetime import datetime, date, timezone
from decimal import Decimal
from urllib3 import connection_from_url
from urllib3.connection import HTTPConnection
Expand Down Expand Up @@ -82,13 +82,17 @@ def super_len(o):

class CrateJsonEncoder(json.JSONEncoder):

epoch = datetime(1970, 1, 1)
epoch_aware = datetime(1970, 1, 1, tzinfo=timezone.utc)
epoch_naive = datetime(1970, 1, 1)

def default(self, o):
if isinstance(o, Decimal):
return str(o)
if isinstance(o, datetime):
delta = o - self.epoch
if o.tzinfo is not None:
delta = o - self.epoch_aware
else:
delta = o - self.epoch_naive
return int(delta.microseconds / 1000.0 +
(delta.seconds + delta.days * 24 * 3600) * 1000.0)
if isinstance(o, date):
Expand Down
15 changes: 14 additions & 1 deletion src/crate/client/test_http.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@
from urllib.parse import urlparse, parse_qs
from setuptools.ssl_support import find_ca_bundle

from .http import Client, _get_socket_opts, _remove_certs_for_non_https
from .http import Client, CrateJsonEncoder, _get_socket_opts, _remove_certs_for_non_https
from .exceptions import ConnectionError, ProgrammingError


Expand Down Expand Up @@ -626,3 +626,16 @@ def test_username(self):
self.assertEqual(TestingHTTPServer.SHARED['usernameFromXUser'], 'testDBUser')
self.assertEqual(TestingHTTPServer.SHARED['username'], 'testDBUser')
self.assertEqual(TestingHTTPServer.SHARED['password'], 'test:password')


class TestCrateJsonEncoder(TestCase):

def test_naive_datetime(self):
data = dt.datetime.fromisoformat("2023-06-26T09:24:00.123")
result = json.dumps(data, cls=CrateJsonEncoder)
self.assertEqual(result, "1687771440123")

def test_aware_datetime(self):
data = dt.datetime.fromisoformat("2023-06-26T09:24:00.123+02:00")
result = json.dumps(data, cls=CrateJsonEncoder)
self.assertEqual(result, "1687764240123")
2 changes: 2 additions & 0 deletions src/crate/client/tests.py
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@
RetryOnTimeoutServerTest,
RequestsCaBundleTest,
TestUsernameSentAsHeader,
TestCrateJsonEncoder,
TestDefaultSchemaHeader,
)
from .sqlalchemy.tests import test_suite as sqlalchemy_test_suite
Expand Down Expand Up @@ -341,6 +342,7 @@ def test_suite():
suite.addTest(unittest.makeSuite(RetryOnTimeoutServerTest))
suite.addTest(unittest.makeSuite(RequestsCaBundleTest))
suite.addTest(unittest.makeSuite(TestUsernameSentAsHeader))
suite.addTest(unittest.makeSuite(TestCrateJsonEncoder))
suite.addTest(unittest.makeSuite(TestDefaultSchemaHeader))
suite.addTest(sqlalchemy_test_suite())
suite.addTest(doctest.DocTestSuite('crate.client.connection'))
Expand Down