diff --git a/ibis/expr/operations.py b/ibis/expr/operations.py index d3647d79327a..226d9caaa0a7 100644 --- a/ibis/expr/operations.py +++ b/ibis/expr/operations.py @@ -2014,7 +2014,7 @@ def _maybe_cast_args(self, left, right): return left, left._implicit_cast(right) if right._can_implicit_cast(left): - return right, right._implicit_cast(left) + return right._implicit_cast(left), right return left, right diff --git a/ibis/expr/tests/test_value_exprs.py b/ibis/expr/tests/test_value_exprs.py index acdb508c2714..92ff6ffba8c9 100644 --- a/ibis/expr/tests/test_value_exprs.py +++ b/ibis/expr/tests/test_value_exprs.py @@ -13,6 +13,7 @@ # limitations under the License. import operator +from datetime import date, datetime import pytest @@ -23,6 +24,7 @@ import ibis.expr.operations as ops import ibis +from ibis import literal from ibis.tests.util import assert_equal @@ -857,3 +859,34 @@ def test_generic_value_api_no_arithmetic(value, operation): ) def test_fillna_null(value, expected): assert ibis.NA.fillna(value).type().equals(expected) + + +@pytest.mark.parametrize( + ('left', 'right'), + [ + (literal('2017-04-01'), date(2017, 4, 2)), + (date(2017, 4, 2), literal('2017-04-01')), + (literal('2017-04-01 01:02:33'), datetime(2017, 4, 1, 1, 3, 34)), + (datetime(2017, 4, 1, 1, 3, 34), literal('2017-04-01 01:02:33')), + ] +) +@pytest.mark.parametrize( + 'op', + [ + operator.eq, + operator.ne, + operator.lt, + operator.le, + operator.gt, + operator.ge, + lambda left, right: ibis.timestamp( + '2017-04-01 00:02:34' + ).between(left, right), + lambda left, right: ibis.timestamp( + '2017-04-01' + ).cast(dt.date).between(left, right) + ] +) +def test_string_temporal_compare(op, left, right): + result = op(left, right) + assert result.type().equals(dt.boolean) diff --git a/ibis/expr/types.py b/ibis/expr/types.py index 9dd98f3df314..20049a1b0122 100644 --- a/ibis/expr/types.py +++ b/ibis/expr/types.py @@ -16,6 +16,7 @@ import datetime import webbrowser import warnings +import sys import six import toolz @@ -241,6 +242,12 @@ def _get_unbound_tables(self): pass +if sys.version_info.major == 2: + # Python 2.7 doesn't return NotImplemented unless the other operand has + # an attribute called "timetuple". This is a bug that's fixed in Python 3 + Expr.timetuple = None + + def _safe_repr(x, memo=None): return x._repr(memo=memo) if isinstance(x, (Expr, Node)) else repr(x) @@ -956,7 +963,7 @@ def type(self): return dt.string def _can_compare(self, other): - return isinstance(other, StringValue) + return isinstance(other, (StringValue, TemporalValue)) class DecimalValue(NumericValue): @@ -978,7 +985,11 @@ def constructor(arg, name=None): return constructor -class DateValue(AnyValue): +class TemporalValue(AnyValue): + pass + + +class DateValue(TemporalValue): def type(self): return dt.date @@ -995,7 +1006,7 @@ def _can_implicit_cast(self, arg): return False def _can_compare(self, other): - return isinstance(other, DateValue) + return isinstance(other, (TemporalValue, StringValue)) def _implicit_cast(self, arg): # assume we've checked this is OK at this point... @@ -1003,7 +1014,7 @@ def _implicit_cast(self, arg): return DateScalar(op) -class TimestampValue(AnyValue): +class TimestampValue(TemporalValue): def __init__(self, meta=None): self.meta = meta @@ -1037,7 +1048,7 @@ def _can_implicit_cast(self, arg): return False def _can_compare(self, other): - return isinstance(other, TimestampValue) + return isinstance(other, (TemporalValue, StringValue)) def _implicit_cast(self, arg): # assume we've checked this is OK at this point... @@ -1320,6 +1331,8 @@ def literal(value, type=None): ... TypeError: Value 'foobar' cannot be safely coerced to int64 """ + if hasattr(value, 'op') and isinstance(value.op(), Literal): + return value if type is None: type = infer_literal_type(value) diff --git a/ibis/sql/postgres/tests/test_functions.py b/ibis/sql/postgres/tests/test_functions.py index e73d41c0b22a..d72319e3bfbe 100644 --- a/ibis/sql/postgres/tests/test_functions.py +++ b/ibis/sql/postgres/tests/test_functions.py @@ -17,6 +17,8 @@ import operator import unittest +from datetime import date, datetime + import pytest import string @@ -1160,3 +1162,75 @@ def test_timestamp_type_accepts_all_timezones(): ] for zone in zones: assert dt.Timestamp(zone).timezone == zone + + +@pytest.mark.postgresql +@pytest.mark.parametrize( + ('left', 'right', 'type'), + [ + (L('2017-04-01'), date(2017, 4, 2), dt.date), + (date(2017, 4, 2), L('2017-04-01'), dt.date), + ( + L('2017-04-01 01:02:33'), + datetime(2017, 4, 1, 1, 3, 34), + dt.timestamp + ), + ( + datetime(2017, 4, 1, 1, 3, 34), + L('2017-04-01 01:02:33'), + dt.timestamp + ), + ] +) +@pytest.mark.parametrize( + 'op', + [ + operator.eq, + operator.ne, + operator.lt, + operator.le, + operator.gt, + operator.ge, + ] +) +def test_string_temporal_compare(con, op, left, right, type): + expr = op(left, right) + result = con.execute(expr) + left_raw = con.execute(L(left).cast(type)) + right_raw = con.execute(L(right).cast(type)) + expected = op(left_raw, right_raw) + assert result == expected + + +@pytest.mark.postgresql +@pytest.mark.parametrize( + ('left', 'right'), + [ + (L('2017-03-31').cast(dt.date), date(2017, 4, 2)), + (date(2017, 3, 31), L('2017-04-02').cast(dt.date)), + ( + L('2017-03-31 00:02:33').cast(dt.timestamp), + datetime(2017, 4, 1, 1, 3, 34), + ), + ( + datetime(2017, 3, 31, 0, 2, 33), + L('2017-04-01 01:03:34').cast(dt.timestamp), + ), + ] +) +@pytest.mark.parametrize( + 'op', + [ + lambda left, right: ibis.timestamp('2017-04-01 00:02:34').between( + left, right + ), + lambda left, right: ibis.timestamp('2017-04-01').cast(dt.date).between( + left, right + ), + ] +) +def test_string_temporal_compare_between(con, op, left, right): + expr = op(left, right) + result = con.execute(expr) + assert isinstance(result, (bool, np.bool_)) + assert result