Skip to content
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
21 changes: 21 additions & 0 deletions docs/data-types.rst
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,26 @@ Data types
``date`` or ``datetime`` objects, timezone information will not be
preserved. If you need to store it, you will need to use a separate column.

.. NOTE::

Inserting timezone-aware ``datetime`` objects is supported; the value is
converted to a UTC instant on the way in, as outlined above. On the way
out, the dialect returns **naive** ``datetime`` objects in UTC by default.

To read values back as timezone-aware ``datetime`` objects instead,
configure the CrateDB driver's ``time_zone`` argument, for example::

from sqlalchemy import create_engine

engine = create_engine(
"crate://localhost:4200",
connect_args={"time_zone": "+0530"},
)

The driver then converts ``TIMESTAMP`` columns to timezone-aware
``datetime`` objects transparently. See `TIMESTAMP conversion with time
zone`_ in the driver documentation for the accepted ``time_zone`` values.


.. _data-types-sqlalchemy:

Expand Down Expand Up @@ -88,4 +108,5 @@ __ https://cratedb.com/docs/crate/reference/en/latest/general/ddl/data-types.htm
__ https://cratedb.com/docs/crate/reference/en/latest/general/ddl/data-types.html#geo-shape


.. _TIMESTAMP conversion with time zone: https://cratedb.com/docs/python/en/latest/query.html#timestamp-conversion-with-time-zone
.. _Unix time: https://en.wikipedia.org/wiki/Unix_time
5 changes: 5 additions & 0 deletions src/sqlalchemy_cratedb/dialect.py
Original file line number Diff line number Diff line change
Expand Up @@ -144,6 +144,11 @@ def result_processor(self, dialect, coltype):
def process(value):
if not value:
return None
# When the driver's `time_zone` is configured, the timestamp column
# has already been converted to `datetime` by the driver's data type converter.
# Pass it through unchanged so this processor stays idempotent.
if isinstance(value, datetime):
return value
try:
return datetime.utcfromtimestamp(value / 1e3)
except TypeError:
Expand Down
35 changes: 35 additions & 0 deletions tests/datetime_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -232,6 +232,41 @@ def test_datetime_date(session):
assert result["datetime_tz"] == dt.datetime(2009, 5, 13, 0, 0, 0)


@pytest.mark.skipif(SA_VERSION < SA_1_4, reason="Test case not supported on SQLAlchemy 1.3")
def test_datetime_tz_aware_read(session, cratedb_service):
"""
With the driver's `time_zone` configured, `TIMESTAMP` columns read back as
timezone-aware `datetime` objects through the dialect.

Regression test for https://github.com/crate/sqlalchemy-cratedb/issues/92:
when the driver converts the column to a `datetime` itself, the dialect's
result processor must pass it through unchanged
"""
aware = dt.datetime(2020, 6, 23, 12, 0, 0, tzinfo=dt.timezone(dt.timedelta(hours=2)))

# Write
session.add(FooBar(name="tz", datetime_tz=aware))
session.commit()
session.execute(sa.text("REFRESH TABLE foobar"))

# Read via an engine whose driver returns timezone-aware datetimes.
aware_engine = sa.create_engine(
cratedb_service.database.engine.url, connect_args={"time_zone": "+0530"}
)
try:
with aware_engine.connect() as conn:
result = conn.execute(
sa.select(FooBar.datetime_tz).where(FooBar.name == "tz")
).scalar_one()
finally:
aware_engine.dispose()

# Same instant (10:00 UTC), now timezone-aware at the requested offset.
assert result.tzinfo is not None
assert result.utcoffset() == dt.timedelta(hours=5, minutes=30)
assert result == dt.datetime(2020, 6, 23, 10, 0, 0, tzinfo=dt.timezone.utc)


@pytest.mark.skipif(SA_VERSION < SA_1_4, reason="Test case not supported on SQLAlchemy 1.3")
def test_time(session):
"""
Expand Down