Skip to content

Commit 3dae1a4

Browse files
committed
Add support for sa.Time type by storing it as a STRING in ISO 8601 format
1 parent 9f6ab04 commit 3dae1a4

4 files changed

Lines changed: 86 additions & 1 deletion

File tree

src/sqlalchemy_cratedb/compiler.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -246,6 +246,15 @@ def visit_datetime(self, type_, **kw):
246246
def visit_date(self, type_, **kw):
247247
return "TIMESTAMP"
248248

249+
def visit_time(self, type_, **kw):
250+
"""
251+
CrateDB has no storable `TIME` column type. Plain `TIME` does not exist,
252+
and `TIME WITH TIME ZONE` (TIMETZ) is literal/cast-only ("does not support
253+
storage"). So store the time-of-day as a `STRING`, holding the value in
254+
ISO 8601 format (e.g. ``19:00:30.123456``).
255+
"""
256+
return "STRING"
257+
249258
def visit_ARRAY(self, type_, **kw):
250259
if type_.dimensions is not None and type_.dimensions > 1:
251260
raise NotImplementedError("CrateDB doesn't support multidimensional arrays")

src/sqlalchemy_cratedb/dialect.py

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@
2121

2222
import logging
2323
import warnings
24-
from datetime import date, datetime
24+
from datetime import date, datetime, time
2525

2626
from sqlalchemy import types as sqltypes
2727
from sqlalchemy.engine import default, reflection
@@ -167,10 +167,31 @@ def process(value):
167167
return process
168168

169169

170+
class Time(sqltypes.Time):
171+
def bind_processor(self, dialect):
172+
def process(value):
173+
if value is not None:
174+
return value.isoformat()
175+
return None
176+
177+
return process
178+
179+
def result_processor(self, dialect, coltype):
180+
def process(value):
181+
if value is None:
182+
return None
183+
if isinstance(value, time):
184+
return value
185+
return time.fromisoformat(value)
186+
187+
return process
188+
189+
170190
colspecs = {
171191
sqltypes.Date: Date,
172192
sqltypes.DateTime: DateTime,
173193
sqltypes.TIMESTAMP: DateTime,
194+
sqltypes.Time: Time,
174195
}
175196

176197
if SA_VERSION >= SA_2_0:

tests/create_table_test.py

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,30 @@ class User(self.Base):
7878
sa.util.immutabledict({}),
7979
)
8080

81+
def test_table_time_type(self):
82+
"""
83+
`sa.Time` has no native CrateDB counterpart and is stored as `STRING`.
84+
85+
Validates the fix for https://github.com/crate/sqlalchemy-cratedb/issues/206,
86+
where `sa.Time` previously compiled to `TIME`, which CrateDB rejects with
87+
`SQLParseException[Cannot find data type: time]`.
88+
"""
89+
90+
class Schedule(self.Base):
91+
__tablename__ = "schedule"
92+
name = sa.Column(sa.String, primary_key=True)
93+
time_col = sa.Column(sa.Time)
94+
95+
self.Base.metadata.create_all(bind=self.engine)
96+
fake_cursor.execute.assert_called_with(
97+
(
98+
"\nCREATE TABLE schedule (\n\tname STRING NOT NULL, "
99+
"\n\ttime_col STRING, "
100+
"\n\tPRIMARY KEY (name)\n)\n\n"
101+
),
102+
sa.util.immutabledict({}),
103+
)
104+
81105
def test_column_obj(self):
82106
class DummyTable(self.Base):
83107
__tablename__ = "dummy"

tests/datetime_test.py

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -105,6 +105,7 @@ class FooBar(Base):
105105
date = sa.Column(sa.Date)
106106
datetime_notz = sa.Column(DateTime(timezone=False))
107107
datetime_tz = sa.Column(DateTime(timezone=True))
108+
time = sa.Column(sa.Time)
108109

109110

110111
@pytest.fixture
@@ -228,3 +229,33 @@ def test_datetime_date(session):
228229
# Compare outcome.
229230
assert result["datetime_notz"] == dt.datetime(2009, 5, 13, 0, 0, 0)
230231
assert result["datetime_tz"] == dt.datetime(2009, 5, 13, 0, 0, 0)
232+
233+
234+
@pytest.mark.skipif(SA_VERSION < SA_1_4, reason="Test case not supported on SQLAlchemy 1.3")
235+
def test_time(session):
236+
"""
237+
An integration test for `sa.Time`.
238+
239+
CrateDB has no native `TIME` type, so the dialect stores it as a `STRING`
240+
in ISO 8601 format and parses it back into a `dt.time` object on read.
241+
242+
Validates the fix for https://github.com/crate/sqlalchemy-cratedb/issues/206.
243+
"""
244+
245+
# insert
246+
foo_item = FooBar(
247+
name="foo",
248+
time=dt.time(19, 0, 30, 123456),
249+
)
250+
session.add(foo_item)
251+
session.commit()
252+
session.execute(sa.text("REFRESH TABLE foobar"))
253+
254+
# query
255+
result = (
256+
session.execute(sa.select(FooBar.name, FooBar.time)).mappings().first()
257+
)
258+
259+
# compare
260+
assert result["time"] == dt.time(19, 0, 30, 123456)
261+
assert isinstance(result["time"], dt.time)

0 commit comments

Comments
 (0)