Skip to content

Commit

Permalink
feat(api): add DateValue.epoch api for computing days since epoch (#…
Browse files Browse the repository at this point in the history
…9856)

Co-authored-by: Jim Crist-Harif <jcristharif@gmail.com>
  • Loading branch information
cpcloud and jcrist authored Aug 26, 2024
1 parent d858ffd commit 8b0fb66
Show file tree
Hide file tree
Showing 8 changed files with 114 additions and 27 deletions.
9 changes: 9 additions & 0 deletions ibis/backends/polars/compiler.py
Original file line number Diff line number Diff line change
Expand Up @@ -1442,3 +1442,12 @@ def execute_group_concat(op, **kw):
arg = arg.sort_by(keys, descending=descending)

return pl.when(arg.count() > 0).then(arg.str.join(sep)).otherwise(None)


@translate.register(ops.DateDelta)
def execute_date_delta(op, **kw):
left = translate(op.left, **kw)
right = translate(op.right, **kw)
delta = left - right
method_name = f"total_{_literal_value(op.part)}s"
return getattr(delta.dt, method_name)()
12 changes: 11 additions & 1 deletion ibis/backends/sql/compilers/impala.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,6 @@ class ImpalaCompiler(SQLGlotCompiler):
ops.ArrayPosition,
ops.Array,
ops.Covariance,
ops.DateDelta,
ops.ExtractDayOfYear,
ops.Levenshtein,
ops.Map,
Expand Down Expand Up @@ -314,5 +313,16 @@ def visit_Sign(self, op, *, arg):
return self.cast(sign, dtype)
return sign

def visit_DateDelta(self, op, *, left, right, part):
if not isinstance(part, sge.Literal):
raise com.UnsupportedOperationError(
"Only literal `part` values are supported for date delta"
)
if part.this != "day":
raise com.UnsupportedOperationError(
f"Only 'day' part is supported for date delta in the {self.dialect} backend"
)
return self.f.datediff(left, right)


compiler = ImpalaCompiler()
18 changes: 17 additions & 1 deletion ibis/backends/sql/compilers/oracle.py
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,6 @@ class OracleCompiler(SQLGlotCompiler):
ops.Bucket,
ops.TimestampBucket,
ops.TimeDelta,
ops.DateDelta,
ops.TimestampDelta,
ops.TimestampFromYMDHMS,
ops.TimeFromHMS,
Expand Down Expand Up @@ -474,5 +473,22 @@ def visit_GroupConcat(self, op, *, arg, where, sep, order_by):
def visit_IntervalFromInteger(self, op, *, arg, unit):
return self._value_to_interval(arg, unit)

def visit_DateFromYMD(self, op, *, year, month, day):
year = self.f.lpad(year, 4, "0")
month = self.f.lpad(month, 2, "0")
day = self.f.lpad(day, 2, "0")
return self.f.to_date(self.f.concat(year, month, day), "FXYYYYMMDD")

def visit_DateDelta(self, op, *, left, right, part):
if not isinstance(part, sge.Literal):
raise com.UnsupportedOperationError(
"Only literal `part` values are supported for date delta"
)
if part.this != "day":
raise com.UnsupportedOperationError(
f"Only 'day' part is supported for date delta in the {self.dialect} backend"
)
return left - right


compiler = OracleCompiler()
1 change: 0 additions & 1 deletion ibis/backends/sql/compilers/risingwave.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,6 @@ class RisingWaveCompiler(PostgresCompiler):

UNSUPPORTED_OPS = (
ops.Arbitrary,
ops.DateFromYMD,
ops.Mode,
ops.RandomUUID,
ops.MultiQuantile,
Expand Down
12 changes: 11 additions & 1 deletion ibis/backends/sql/compilers/sqlite.py
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,6 @@ class SQLiteCompiler(SQLGlotCompiler):
ops.StringToDate,
ops.StringToTimestamp,
ops.TimeDelta,
ops.DateDelta,
ops.TimestampDelta,
ops.TryCast,
)
Expand Down Expand Up @@ -531,5 +530,16 @@ def visit_NonNullLiteral(self, op, *, value, dtype):
raise com.UnsupportedBackendType(f"Unsupported type: {dtype!r}")
return super().visit_NonNullLiteral(op, value=value, dtype=dtype)

def visit_DateDelta(self, op, *, left, right, part):
if not isinstance(part, sge.Literal):
raise com.UnsupportedOperationError(
"Only literal `part` values are supported for date delta"
)
if part.this != "day":
raise com.UnsupportedOperationError(
f"Only 'day' part is supported for date delta in the {self.dialect} backend"
)
return self.f._ibis_date_delta(left, right)


compiler = SQLiteCompiler()
7 changes: 7 additions & 0 deletions ibis/backends/sqlite/udf.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
import math
import operator
from collections import defaultdict
from datetime import date
from typing import TYPE_CHECKING, Any, NamedTuple
from urllib.parse import parse_qs, urlsplit
from uuid import uuid4
Expand Down Expand Up @@ -357,6 +358,12 @@ def _ibis_extract_user_info(url):
return f"{username}:{password}"


@udf
def _ibis_date_delta(left, right):
delta = date.fromisoformat(left) - date.fromisoformat(right)
return delta.days


class _ibis_var:
def __init__(self, offset):
self.mean = 0.0
Expand Down
46 changes: 23 additions & 23 deletions ibis/backends/tests/test_temporal.py
Original file line number Diff line number Diff line change
Expand Up @@ -1474,11 +1474,7 @@ def test_today_from_projection(alltypes):


@pytest.mark.notimpl(
["pandas", "dask", "exasol", "risingwave", "druid"],
raises=com.OperationNotDefinedError,
)
@pytest.mark.notimpl(
["oracle"], raises=OracleDatabaseError, reason="ORA-00936 missing expression"
["pandas", "dask", "exasol", "druid"], raises=com.OperationNotDefinedError
)
def test_date_literal(con, backend):
expr = ibis.date(2022, 2, 4)
Expand Down Expand Up @@ -1709,11 +1705,7 @@ def test_interval_literal(con, backend):


@pytest.mark.notimpl(
["pandas", "dask", "exasol", "risingwave", "druid"],
raises=com.OperationNotDefinedError,
)
@pytest.mark.notimpl(
["oracle"], raises=OracleDatabaseError, reason="ORA-00936: missing expression"
["pandas", "dask", "exasol", "druid"], raises=com.OperationNotDefinedError
)
def test_date_column_from_ymd(backend, con, alltypes, df):
c = alltypes.timestamp_col
Expand Down Expand Up @@ -1975,16 +1967,7 @@ def test_timestamp_precision_output(con, ts, scale, unit):


@pytest.mark.notimpl(
[
"dask",
"datafusion",
"druid",
"impala",
"oracle",
"pandas",
"polars",
],
raises=com.OperationNotDefinedError,
["dask", "datafusion", "druid", "pandas"], raises=com.OperationNotDefinedError
)
@pytest.mark.parametrize(
("start", "end", "unit", "expected"),
Expand All @@ -2006,7 +1989,10 @@ def test_timestamp_precision_output(con, ts, scale, unit):
reason="postgres doesn't have any easy way to accurately compute the delta in specific units",
raises=com.OperationNotDefinedError,
),
pytest.mark.notimpl(["exasol"], raises=com.OperationNotDefinedError),
pytest.mark.notimpl(
["exasol", "polars", "sqlite", "oracle", "impala"],
raises=com.OperationNotDefinedError,
),
],
),
param(ibis.date("1992-09-30"), ibis.date("1992-10-01"), "day", 1, id="date"),
Expand All @@ -2027,12 +2013,14 @@ def test_timestamp_precision_output(con, ts, scale, unit):
raises=com.OperationNotDefinedError,
reason="timestampdiff rounds after subtraction and mysql doesn't have a date_trunc function",
),
pytest.mark.notimpl(["exasol"], raises=com.OperationNotDefinedError),
pytest.mark.notimpl(
["exasol", "polars", "sqlite", "oracle", "impala"],
raises=com.OperationNotDefinedError,
),
],
),
],
)
@pytest.mark.notimpl(["sqlite"], raises=com.OperationNotDefinedError)
def test_delta(con, start, end, unit, expected):
expr = end.delta(start, unit)
assert con.execute(expr) == expected
Expand Down Expand Up @@ -2297,3 +2285,15 @@ def test_date_scalar(con, value, func):
assert isinstance(result, datetime.date)

assert result == datetime.date.fromisoformat(value)


@pytest.mark.notyet(
["dask", "datafusion", "pandas", "druid", "exasol"],
raises=com.OperationNotDefinedError,
)
def test_simple_unix_date_offset(con):
d = ibis.date("2023-04-07")
expr = d.epoch_days()
result = con.execute(expr)
delta = datetime.date(2023, 4, 7) - datetime.date(1970, 1, 1)
assert result == delta.days
36 changes: 36 additions & 0 deletions ibis/expr/types/temporal.py
Original file line number Diff line number Diff line change
Expand Up @@ -474,6 +474,42 @@ def delta(
"""
return ops.DateDelta(left=self, right=other, part=part).to_expr()

def epoch_days(self) -> ir.IntegerValue:
"""Return the number of days since the UNIX epoch date.
Examples
--------
>>> import ibis
>>> ibis.options.interactive = True
>>> date = ibis.date(2020, 1, 1)
>>> date
┌────────────┐
│ 2020-01-01 │
└────────────┘
>>> date.epoch_days()
┌───────┐
│ 18262 │
└───────┘
>>> t = date.name("date_col").as_table()
>>> t
┏━━━━━━━━━━━━┓
┃ date_col ┃
┡━━━━━━━━━━━━┩
│ date │
├────────────┤
│ 2020-01-01 │
└────────────┘
>>> t.mutate(epoch=t.date_col.epoch_days())
┏━━━━━━━━━━━━┳━━━━━━━┓
┃ date_col ┃ epoch ┃
┡━━━━━━━━━━━━╇━━━━━━━┩
│ date │ int64 │
├────────────┼───────┤
│ 2020-01-01 │ 18262 │
└────────────┴───────┘
"""
return self.delta(ibis.date(1970, 1, 1), "day")


@public
class DateScalar(Scalar, DateValue):
Expand Down

0 comments on commit 8b0fb66

Please sign in to comment.