Skip to content

Commit cfd65a0

Browse files
gforsythcpcloud
authored andcommitted
feat(sqlite): add ops.DateSub, ops.DateAdd, ops.DateDiff
A few missing ops to help clean up some of our TPC-H queries.
1 parent c550780 commit cfd65a0

File tree

2 files changed

+61
-7
lines changed

2 files changed

+61
-7
lines changed

ibis/backends/sqlite/registry.py

Lines changed: 43 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
)
2323
from ibis.backends.base.sql.alchemy.registry import _gen_string_find
2424
from ibis.backends.base.sql.alchemy.registry import _literal as base_literal
25+
from ibis.common.enums import DateUnit, IntervalUnit
2526

2627
operation_registry = sqlalchemy_operation_registry.copy()
2728
operation_registry.update(sqlalchemy_window_functions_registry)
@@ -93,18 +94,22 @@ def _extract_quarter(t, op):
9394

9495

9596
_truncate_modifiers = {
96-
'Y': 'start of year',
97-
'M': 'start of month',
98-
'D': 'start of day',
99-
'W': 'weekday 1',
97+
DateUnit.DAY: 'start of day',
98+
DateUnit.WEEK: 'weekday 1',
99+
DateUnit.MONTH: 'start of month',
100+
DateUnit.YEAR: 'start of year',
101+
IntervalUnit.DAY: 'start of day',
102+
IntervalUnit.WEEK: 'weekday 1',
103+
IntervalUnit.MONTH: 'start of month',
104+
IntervalUnit.YEAR: 'start of year',
100105
}
101106

102107

103108
def _truncate(func):
104109
def translator(t, op):
105110
sa_arg = t.translate(op.arg)
106111
try:
107-
modifier = _truncate_modifiers[op.unit.short]
112+
modifier = _truncate_modifiers[op.unit]
108113
except KeyError:
109114
raise com.UnsupportedOperationError(
110115
f'Unsupported truncate unit {op.unit!r}'
@@ -208,6 +213,36 @@ def _arbitrary(t, op):
208213
return reduction(getattr(sa.func, f"_ibis_sqlite_arbitrary_{how}"))(t, op)
209214

210215

216+
_INTERVAL_DATE_UNITS = frozenset(
217+
(IntervalUnit.YEAR, IntervalUnit.MONTH, IntervalUnit.DAY)
218+
)
219+
220+
221+
def _timestamp_op(func, sign, units):
222+
def _formatter(translator, op):
223+
arg, offset = op.args
224+
225+
unit = offset.output_dtype.unit
226+
if unit not in units:
227+
raise com.UnsupportedOperationError(
228+
"SQLite does not allow binary operation "
229+
f"{func} with INTERVAL offset {unit}"
230+
)
231+
offset = translator.translate(offset)
232+
result = getattr(sa.func, func)(
233+
translator.translate(arg),
234+
f"{sign}{offset.value} {unit.plural}",
235+
)
236+
return result
237+
238+
return _formatter
239+
240+
241+
def _date_diff(t, op):
242+
left, right = map(t.translate, op.args)
243+
return sa.func.julianday(left) - sa.func.julianday(right)
244+
245+
211246
operation_registry.update(
212247
{
213248
# TODO(kszucs): don't dispatch on op.arg since that should be always an
@@ -242,6 +277,9 @@ def _arbitrary(t, op):
242277
),
243278
ops.DateTruncate: _truncate(sa.func.date),
244279
ops.Date: unary(sa.func.date),
280+
ops.DateAdd: _timestamp_op("DATE", "+", _INTERVAL_DATE_UNITS),
281+
ops.DateSub: _timestamp_op("DATE", "-", _INTERVAL_DATE_UNITS),
282+
ops.DateDiff: _date_diff,
245283
ops.Time: unary(sa.func.time),
246284
ops.TimestampTruncate: _truncate(sa.func.datetime),
247285
ops.Strftime: fixed_arity(

ibis/backends/tests/test_temporal.py

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -710,6 +710,13 @@ def convert_to_offset(offset, displacement_type=displacement_type):
710710
raises=com.UnsupportedOperationError,
711711
reason="Interval from integer column is unsupported for the PySpark backend.",
712712
)
713+
@pytest.mark.notimpl(
714+
[
715+
"sqlite",
716+
],
717+
raises=(com.UnsupportedOperationError, com.OperationNotDefinedError),
718+
reason="Handling unsupported op error for DateAdd with weeks",
719+
)
713720
def test_integer_to_interval_date(backend, con, alltypes, df, unit):
714721
interval = alltypes.int_col.to_interval(unit=unit)
715722
array = alltypes.date_string_col.split('/')
@@ -751,6 +758,10 @@ def convert_to_offset(x):
751758
["bigquery"],
752759
raises=com.UnsupportedOperationError,
753760
),
761+
pytest.mark.notimpl(
762+
["sqlite"],
763+
raises=com.OperationNotDefinedError,
764+
),
754765
pytest.mark.notimpl(
755766
["druid"],
756767
raises=com.IbisTypeError,
@@ -774,6 +785,7 @@ def convert_to_offset(x):
774785
"pandas",
775786
"postgres",
776787
"snowflake",
788+
"sqlite",
777789
],
778790
raises=com.OperationNotDefinedError,
779791
),
@@ -801,6 +813,10 @@ def convert_to_offset(x):
801813
raises=TypeError,
802814
reason="unsupported operand type(s) for -: 'StringColumn' and 'IntervalScalar'",
803815
),
816+
pytest.mark.notimpl(
817+
["sqlite"],
818+
raises=com.OperationNotDefinedError,
819+
),
804820
],
805821
),
806822
param(
@@ -837,7 +853,7 @@ def convert_to_offset(x):
837853
id='timestamp-subtract-timestamp',
838854
marks=[
839855
pytest.mark.notimpl(
840-
["bigquery", "snowflake"],
856+
["bigquery", "snowflake", "sqlite"],
841857
raises=com.OperationNotDefinedError,
842858
),
843859
pytest.mark.notimpl(
@@ -868,7 +884,7 @@ def convert_to_offset(x):
868884
],
869885
)
870886
@pytest.mark.notimpl(
871-
["datafusion", "sqlite", "mssql", "trino"],
887+
["datafusion", "mssql", "trino"],
872888
raises=com.OperationNotDefinedError,
873889
)
874890
def test_temporal_binop(backend, con, alltypes, df, expr_fn, expected_fn):

0 commit comments

Comments
 (0)