Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(api): add DateValue.epoch api for computing days since epoch #9856

Merged
merged 26 commits into from
Aug 26, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
252ce2d
test(date): add simple test for days since epoch
cpcloud Aug 16, 2024
8f635ce
feat(api): define `epoch` method on `DateValue`
cpcloud Aug 16, 2024
5385e30
feat(sql): implement `UnixDate`
cpcloud Aug 16, 2024
0dbf076
feat(bigquery): implement `UnixDate`
cpcloud Aug 16, 2024
e2bc6ad
chore(datafusion): unix date is unsupported
cpcloud Aug 16, 2024
4d49401
feat(sqlite): add date_from_parts impl to support epoch api
cpcloud Aug 16, 2024
8b08b41
feat(clickhouse): add date_from_parts impl to support epoch api
cpcloud Aug 16, 2024
ea27331
feat(mysql): add date_from_parts impl to support epoch api
cpcloud Aug 16, 2024
542a558
chore(trino): cleanup date from parts support
cpcloud Aug 16, 2024
40aca0b
chore(impala): cleanup date from parts support
cpcloud Aug 16, 2024
70aee6a
chore(oracle): cleanup date from parts support
cpcloud Aug 16, 2024
b653e47
feat(flink): cleanup date from parts support
cpcloud Aug 16, 2024
4e9cf7a
test: xfail druid and exasol for unixdate op
cpcloud Aug 16, 2024
48479b1
chore: name to `epoch_days`
cpcloud Aug 23, 2024
988de94
chore: fix doctest
cpcloud Aug 23, 2024
2b7cddc
chore: better docstring
cpcloud Aug 23, 2024
c3bfbca
chore: remove epochdays op
cpcloud Aug 23, 2024
47832eb
chore: revert dialect changes
cpcloud Aug 23, 2024
e2950cc
chore: revert dialect changes
cpcloud Aug 23, 2024
fd9752c
chore: implement date delta
cpcloud Aug 23, 2024
66fdc52
chore: polars
cpcloud Aug 23, 2024
a52f688
feat(risingwave): enable date from ymd
cpcloud Aug 24, 2024
978f55b
feat(sqlite): `epoch_days`
cpcloud Aug 24, 2024
1d8e68a
feat(oracle): date from ymd
cpcloud Aug 24, 2024
36d5e05
feat(oracle): `epoch_days`
cpcloud Aug 24, 2024
1502091
feat(impala): `epoch_days`
cpcloud Aug 24, 2024
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
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 @@
ops.ArrayPosition,
ops.Array,
ops.Covariance,
ops.DateDelta,
ops.ExtractDayOfYear,
ops.Levenshtein,
ops.Map,
Expand Down Expand Up @@ -314,5 +313,16 @@
return self.cast(sign, dtype)
return sign

def visit_DateDelta(self, op, *, left, right, part):
if not isinstance(part, sge.Literal):
raise com.UnsupportedOperationError(

Check warning on line 318 in ibis/backends/sql/compilers/impala.py

View check run for this annotation

Codecov / codecov/patch

ibis/backends/sql/compilers/impala.py#L318

Added line #L318 was not covered by tests
"Only literal `part` values are supported for date delta"
)
if part.this != "day":
raise com.UnsupportedOperationError(

Check warning on line 322 in ibis/backends/sql/compilers/impala.py

View check run for this annotation

Codecov / codecov/patch

ibis/backends/sql/compilers/impala.py#L322

Added line #L322 was not covered by tests
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 @@
ops.Bucket,
ops.TimestampBucket,
ops.TimeDelta,
ops.DateDelta,
ops.TimestampDelta,
ops.TimestampFromYMDHMS,
ops.TimeFromHMS,
Expand Down Expand Up @@ -475,5 +474,22 @@
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(

Check warning on line 485 in ibis/backends/sql/compilers/oracle.py

View check run for this annotation

Codecov / codecov/patch

ibis/backends/sql/compilers/oracle.py#L485

Added line #L485 was not covered by tests
"Only literal `part` values are supported for date delta"
)
if part.this != "day":
raise com.UnsupportedOperationError(

Check warning on line 489 in ibis/backends/sql/compilers/oracle.py

View check run for this annotation

Codecov / codecov/patch

ibis/backends/sql/compilers/oracle.py#L489

Added line #L489 was not covered by tests
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 @@
ops.StringToDate,
ops.StringToTimestamp,
ops.TimeDelta,
ops.DateDelta,
ops.TimestampDelta,
ops.TryCast,
)
Expand Down Expand Up @@ -531,5 +530,16 @@
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(

Check warning on line 535 in ibis/backends/sql/compilers/sqlite.py

View check run for this annotation

Codecov / codecov/patch

ibis/backends/sql/compilers/sqlite.py#L535

Added line #L535 was not covered by tests
"Only literal `part` values are supported for date delta"
)
if part.this != "day":
raise com.UnsupportedOperationError(

Check warning on line 539 in ibis/backends/sql/compilers/sqlite.py

View check run for this annotation

Codecov / codecov/patch

ibis/backends/sql/compilers/sqlite.py#L539

Added line #L539 was not covered by tests
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