Skip to content

Commit 6235bcc

Browse files
committed
Allow handling timezone-aware datetime values when inserting or updating
1 parent cac7e2b commit 6235bcc

File tree

6 files changed

+58
-5
lines changed

6 files changed

+58
-5
lines changed

CHANGES.txt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@ Unreleased
1111
- SQLAlchemy DDL: Allow setting ``server_default`` on columns to enable
1212
server-generated defaults. Thanks, @JanLikar.
1313

14+
- Allow handling "aware" datetime values with time zone info when inserting or updating.
15+
1416

1517
2023/04/18 0.31.1
1618
=================

docs/by-example/client.rst

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -139,6 +139,24 @@ Refresh locations:
139139

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

142+
Updating Data
143+
=============
144+
145+
Both when inserting or updating data, values for ``TIMESTAMP`` columns can be obtained
146+
in different formats. Both literal strings and datetime objects are supported.
147+
148+
>>> import datetime as dt
149+
>>> timestamp_full = "2023-06-26T09:24:00.123+02:00"
150+
>>> timestamp_date = "2023-06-26"
151+
>>> datetime_aware = dt.datetime.fromisoformat("2023-06-26T09:24:00.123+02:00")
152+
>>> datetime_naive = dt.datetime.fromisoformat("2023-06-26T09:24:00.123")
153+
>>> datetime_date = dt.date.fromisoformat("2023-06-26")
154+
>>> cursor.execute("UPDATE locations SET date=? WHERE name='Cloverleaf'", (timestamp_full, ))
155+
>>> cursor.execute("UPDATE locations SET date=? WHERE name='Cloverleaf'", (timestamp_date, ))
156+
>>> cursor.execute("UPDATE locations SET date=? WHERE name='Cloverleaf'", (datetime_aware, ))
157+
>>> cursor.execute("UPDATE locations SET date=? WHERE name='Cloverleaf'", (datetime_naive, ))
158+
>>> cursor.execute("UPDATE locations SET date=? WHERE name='Cloverleaf'", (datetime_date, ))
159+
142160
Selecting Data
143161
==============
144162

docs/data-types.rst

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -94,9 +94,20 @@ __ https://crate.io/docs/crate/reference/en/latest/general/ddl/data-types.html#c
9494

9595
.. NOTE::
9696

97-
The type that ``date`` and ``datetime`` objects are mapped depends on the
97+
The type that ``date`` and ``datetime`` objects are mapped to, depends on the
9898
CrateDB column type.
9999

100+
.. NOTE::
101+
102+
Values of ``TIMESTAMP`` columns will always be stored using a ``LONG`` type,
103+
representing the `Unix time`_ (epoch) timestamp, i.e. number of seconds which
104+
have passed since 00:00:00 UTC on Thursday, 1 January 1970.
105+
106+
This means, when inserting or updating records using timezone-aware Python
107+
``datetime`` objects, timezone information will not be preserved. If you
108+
need to store it, you will need to use a separate column.
109+
110+
100111
.. _data-types-sqlalchemy:
101112

102113
SQLAlchemy
@@ -156,3 +167,6 @@ __ https://crate.io/docs/crate/reference/en/latest/general/ddl/data-types.html#o
156167
__ https://crate.io/docs/crate/reference/en/latest/general/ddl/data-types.html#array
157168
__ https://crate.io/docs/crate/reference/en/latest/general/ddl/data-types.html#geo-point
158169
__ https://crate.io/docs/crate/reference/en/latest/general/ddl/data-types.html#geo-shape
170+
171+
172+
.. _Unix time: https://en.wikipedia.org/wiki/Unix_time

src/crate/client/http.py

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@
3333
from urllib.parse import urlparse
3434
from base64 import b64encode
3535
from time import time
36-
from datetime import datetime, date
36+
from datetime import datetime, date, timezone
3737
from decimal import Decimal
3838
from urllib3 import connection_from_url
3939
from urllib3.connection import HTTPConnection
@@ -82,13 +82,17 @@ def super_len(o):
8282

8383
class CrateJsonEncoder(json.JSONEncoder):
8484

85-
epoch = datetime(1970, 1, 1)
85+
epoch_aware = datetime(1970, 1, 1, tzinfo=timezone.utc)
86+
epoch_naive = datetime(1970, 1, 1)
8687

8788
def default(self, o):
8889
if isinstance(o, Decimal):
8990
return str(o)
9091
if isinstance(o, datetime):
91-
delta = o - self.epoch
92+
if o.tzinfo is not None:
93+
delta = o - self.epoch_aware
94+
else:
95+
delta = o - self.epoch_naive
9296
return int(delta.microseconds / 1000.0 +
9397
(delta.seconds + delta.days * 24 * 3600) * 1000.0)
9498
if isinstance(o, date):

src/crate/client/test_http.py

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@
4040
from urllib.parse import urlparse, parse_qs
4141
from setuptools.ssl_support import find_ca_bundle
4242

43-
from .http import Client, _get_socket_opts, _remove_certs_for_non_https
43+
from .http import Client, CrateJsonEncoder, _get_socket_opts, _remove_certs_for_non_https
4444
from .exceptions import ConnectionError, ProgrammingError
4545

4646

@@ -626,3 +626,16 @@ def test_username(self):
626626
self.assertEqual(TestingHTTPServer.SHARED['usernameFromXUser'], 'testDBUser')
627627
self.assertEqual(TestingHTTPServer.SHARED['username'], 'testDBUser')
628628
self.assertEqual(TestingHTTPServer.SHARED['password'], 'test:password')
629+
630+
631+
class TestCrateJsonEncoder(TestCase):
632+
633+
def test_naive_datetime(self):
634+
data = dt.datetime.fromisoformat("2023-06-26T09:24:00.123")
635+
result = json.dumps(data, cls=CrateJsonEncoder)
636+
self.assertEqual(result, "1687771440123")
637+
638+
def test_aware_datetime(self):
639+
data = dt.datetime.fromisoformat("2023-06-26T09:24:00.123+02:00")
640+
result = json.dumps(data, cls=CrateJsonEncoder)
641+
self.assertEqual(result, "1687764240123")

src/crate/client/tests.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@
5151
RetryOnTimeoutServerTest,
5252
RequestsCaBundleTest,
5353
TestUsernameSentAsHeader,
54+
TestCrateJsonEncoder,
5455
TestDefaultSchemaHeader,
5556
)
5657
from .sqlalchemy.tests import test_suite as sqlalchemy_test_suite
@@ -341,6 +342,7 @@ def test_suite():
341342
suite.addTest(unittest.makeSuite(RetryOnTimeoutServerTest))
342343
suite.addTest(unittest.makeSuite(RequestsCaBundleTest))
343344
suite.addTest(unittest.makeSuite(TestUsernameSentAsHeader))
345+
suite.addTest(unittest.makeSuite(TestCrateJsonEncoder))
344346
suite.addTest(unittest.makeSuite(TestDefaultSchemaHeader))
345347
suite.addTest(sqlalchemy_test_suite())
346348
suite.addTest(doctest.DocTestSuite('crate.client.connection'))

0 commit comments

Comments
 (0)