Skip to content

Commit 8b0fb66

Browse files
cpcloudjcrist
andauthored
feat(api): add DateValue.epoch api for computing days since epoch (#9856)
Co-authored-by: Jim Crist-Harif <[email protected]>
1 parent d858ffd commit 8b0fb66

File tree

8 files changed

+114
-27
lines changed

8 files changed

+114
-27
lines changed

ibis/backends/polars/compiler.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1442,3 +1442,12 @@ def execute_group_concat(op, **kw):
14421442
arg = arg.sort_by(keys, descending=descending)
14431443

14441444
return pl.when(arg.count() > 0).then(arg.str.join(sep)).otherwise(None)
1445+
1446+
1447+
@translate.register(ops.DateDelta)
1448+
def execute_date_delta(op, **kw):
1449+
left = translate(op.left, **kw)
1450+
right = translate(op.right, **kw)
1451+
delta = left - right
1452+
method_name = f"total_{_literal_value(op.part)}s"
1453+
return getattr(delta.dt, method_name)()

ibis/backends/sql/compilers/impala.py

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,6 @@ class ImpalaCompiler(SQLGlotCompiler):
2929
ops.ArrayPosition,
3030
ops.Array,
3131
ops.Covariance,
32-
ops.DateDelta,
3332
ops.ExtractDayOfYear,
3433
ops.Levenshtein,
3534
ops.Map,
@@ -314,5 +313,16 @@ def visit_Sign(self, op, *, arg):
314313
return self.cast(sign, dtype)
315314
return sign
316315

316+
def visit_DateDelta(self, op, *, left, right, part):
317+
if not isinstance(part, sge.Literal):
318+
raise com.UnsupportedOperationError(
319+
"Only literal `part` values are supported for date delta"
320+
)
321+
if part.this != "day":
322+
raise com.UnsupportedOperationError(
323+
f"Only 'day' part is supported for date delta in the {self.dialect} backend"
324+
)
325+
return self.f.datediff(left, right)
326+
317327

318328
compiler = ImpalaCompiler()

ibis/backends/sql/compilers/oracle.py

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,6 @@ class OracleCompiler(SQLGlotCompiler):
6363
ops.Bucket,
6464
ops.TimestampBucket,
6565
ops.TimeDelta,
66-
ops.DateDelta,
6766
ops.TimestampDelta,
6867
ops.TimestampFromYMDHMS,
6968
ops.TimeFromHMS,
@@ -474,5 +473,22 @@ def visit_GroupConcat(self, op, *, arg, where, sep, order_by):
474473
def visit_IntervalFromInteger(self, op, *, arg, unit):
475474
return self._value_to_interval(arg, unit)
476475

476+
def visit_DateFromYMD(self, op, *, year, month, day):
477+
year = self.f.lpad(year, 4, "0")
478+
month = self.f.lpad(month, 2, "0")
479+
day = self.f.lpad(day, 2, "0")
480+
return self.f.to_date(self.f.concat(year, month, day), "FXYYYYMMDD")
481+
482+
def visit_DateDelta(self, op, *, left, right, part):
483+
if not isinstance(part, sge.Literal):
484+
raise com.UnsupportedOperationError(
485+
"Only literal `part` values are supported for date delta"
486+
)
487+
if part.this != "day":
488+
raise com.UnsupportedOperationError(
489+
f"Only 'day' part is supported for date delta in the {self.dialect} backend"
490+
)
491+
return left - right
492+
477493

478494
compiler = OracleCompiler()

ibis/backends/sql/compilers/risingwave.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,6 @@ class RisingWaveCompiler(PostgresCompiler):
1919

2020
UNSUPPORTED_OPS = (
2121
ops.Arbitrary,
22-
ops.DateFromYMD,
2322
ops.Mode,
2423
ops.RandomUUID,
2524
ops.MultiQuantile,

ibis/backends/sql/compilers/sqlite.py

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,6 @@ class SQLiteCompiler(SQLGlotCompiler):
5959
ops.StringToDate,
6060
ops.StringToTimestamp,
6161
ops.TimeDelta,
62-
ops.DateDelta,
6362
ops.TimestampDelta,
6463
ops.TryCast,
6564
)
@@ -531,5 +530,16 @@ def visit_NonNullLiteral(self, op, *, value, dtype):
531530
raise com.UnsupportedBackendType(f"Unsupported type: {dtype!r}")
532531
return super().visit_NonNullLiteral(op, value=value, dtype=dtype)
533532

533+
def visit_DateDelta(self, op, *, left, right, part):
534+
if not isinstance(part, sge.Literal):
535+
raise com.UnsupportedOperationError(
536+
"Only literal `part` values are supported for date delta"
537+
)
538+
if part.this != "day":
539+
raise com.UnsupportedOperationError(
540+
f"Only 'day' part is supported for date delta in the {self.dialect} backend"
541+
)
542+
return self.f._ibis_date_delta(left, right)
543+
534544

535545
compiler = SQLiteCompiler()

ibis/backends/sqlite/udf.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
import math
66
import operator
77
from collections import defaultdict
8+
from datetime import date
89
from typing import TYPE_CHECKING, Any, NamedTuple
910
from urllib.parse import parse_qs, urlsplit
1011
from uuid import uuid4
@@ -357,6 +358,12 @@ def _ibis_extract_user_info(url):
357358
return f"{username}:{password}"
358359

359360

361+
@udf
362+
def _ibis_date_delta(left, right):
363+
delta = date.fromisoformat(left) - date.fromisoformat(right)
364+
return delta.days
365+
366+
360367
class _ibis_var:
361368
def __init__(self, offset):
362369
self.mean = 0.0

ibis/backends/tests/test_temporal.py

Lines changed: 23 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1474,11 +1474,7 @@ def test_today_from_projection(alltypes):
14741474

14751475

14761476
@pytest.mark.notimpl(
1477-
["pandas", "dask", "exasol", "risingwave", "druid"],
1478-
raises=com.OperationNotDefinedError,
1479-
)
1480-
@pytest.mark.notimpl(
1481-
["oracle"], raises=OracleDatabaseError, reason="ORA-00936 missing expression"
1477+
["pandas", "dask", "exasol", "druid"], raises=com.OperationNotDefinedError
14821478
)
14831479
def test_date_literal(con, backend):
14841480
expr = ibis.date(2022, 2, 4)
@@ -1709,11 +1705,7 @@ def test_interval_literal(con, backend):
17091705

17101706

17111707
@pytest.mark.notimpl(
1712-
["pandas", "dask", "exasol", "risingwave", "druid"],
1713-
raises=com.OperationNotDefinedError,
1714-
)
1715-
@pytest.mark.notimpl(
1716-
["oracle"], raises=OracleDatabaseError, reason="ORA-00936: missing expression"
1708+
["pandas", "dask", "exasol", "druid"], raises=com.OperationNotDefinedError
17171709
)
17181710
def test_date_column_from_ymd(backend, con, alltypes, df):
17191711
c = alltypes.timestamp_col
@@ -1975,16 +1967,7 @@ def test_timestamp_precision_output(con, ts, scale, unit):
19751967

19761968

19771969
@pytest.mark.notimpl(
1978-
[
1979-
"dask",
1980-
"datafusion",
1981-
"druid",
1982-
"impala",
1983-
"oracle",
1984-
"pandas",
1985-
"polars",
1986-
],
1987-
raises=com.OperationNotDefinedError,
1970+
["dask", "datafusion", "druid", "pandas"], raises=com.OperationNotDefinedError
19881971
)
19891972
@pytest.mark.parametrize(
19901973
("start", "end", "unit", "expected"),
@@ -2006,7 +1989,10 @@ def test_timestamp_precision_output(con, ts, scale, unit):
20061989
reason="postgres doesn't have any easy way to accurately compute the delta in specific units",
20071990
raises=com.OperationNotDefinedError,
20081991
),
2009-
pytest.mark.notimpl(["exasol"], raises=com.OperationNotDefinedError),
1992+
pytest.mark.notimpl(
1993+
["exasol", "polars", "sqlite", "oracle", "impala"],
1994+
raises=com.OperationNotDefinedError,
1995+
),
20101996
],
20111997
),
20121998
param(ibis.date("1992-09-30"), ibis.date("1992-10-01"), "day", 1, id="date"),
@@ -2027,12 +2013,14 @@ def test_timestamp_precision_output(con, ts, scale, unit):
20272013
raises=com.OperationNotDefinedError,
20282014
reason="timestampdiff rounds after subtraction and mysql doesn't have a date_trunc function",
20292015
),
2030-
pytest.mark.notimpl(["exasol"], raises=com.OperationNotDefinedError),
2016+
pytest.mark.notimpl(
2017+
["exasol", "polars", "sqlite", "oracle", "impala"],
2018+
raises=com.OperationNotDefinedError,
2019+
),
20312020
],
20322021
),
20332022
],
20342023
)
2035-
@pytest.mark.notimpl(["sqlite"], raises=com.OperationNotDefinedError)
20362024
def test_delta(con, start, end, unit, expected):
20372025
expr = end.delta(start, unit)
20382026
assert con.execute(expr) == expected
@@ -2297,3 +2285,15 @@ def test_date_scalar(con, value, func):
22972285
assert isinstance(result, datetime.date)
22982286

22992287
assert result == datetime.date.fromisoformat(value)
2288+
2289+
2290+
@pytest.mark.notyet(
2291+
["dask", "datafusion", "pandas", "druid", "exasol"],
2292+
raises=com.OperationNotDefinedError,
2293+
)
2294+
def test_simple_unix_date_offset(con):
2295+
d = ibis.date("2023-04-07")
2296+
expr = d.epoch_days()
2297+
result = con.execute(expr)
2298+
delta = datetime.date(2023, 4, 7) - datetime.date(1970, 1, 1)
2299+
assert result == delta.days

ibis/expr/types/temporal.py

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -474,6 +474,42 @@ def delta(
474474
"""
475475
return ops.DateDelta(left=self, right=other, part=part).to_expr()
476476

477+
def epoch_days(self) -> ir.IntegerValue:
478+
"""Return the number of days since the UNIX epoch date.
479+
480+
Examples
481+
--------
482+
>>> import ibis
483+
>>> ibis.options.interactive = True
484+
>>> date = ibis.date(2020, 1, 1)
485+
>>> date
486+
┌────────────┐
487+
│ 2020-01-01 │
488+
└────────────┘
489+
>>> date.epoch_days()
490+
┌───────┐
491+
│ 18262 │
492+
└───────┘
493+
>>> t = date.name("date_col").as_table()
494+
>>> t
495+
┏━━━━━━━━━━━━┓
496+
┃ date_col ┃
497+
┡━━━━━━━━━━━━┩
498+
│ date │
499+
├────────────┤
500+
│ 2020-01-01 │
501+
└────────────┘
502+
>>> t.mutate(epoch=t.date_col.epoch_days())
503+
┏━━━━━━━━━━━━┳━━━━━━━┓
504+
┃ date_col ┃ epoch ┃
505+
┡━━━━━━━━━━━━╇━━━━━━━┩
506+
│ date │ int64 │
507+
├────────────┼───────┤
508+
│ 2020-01-01 │ 18262 │
509+
└────────────┴───────┘
510+
"""
511+
return self.delta(ibis.date(1970, 1, 1), "day")
512+
477513

478514
@public
479515
class DateScalar(Scalar, DateValue):

0 commit comments

Comments
 (0)