From 711bf9f973546a4074ee29d0d491067bdf9271b3 Mon Sep 17 00:00:00 2001 From: Jim Crist-Harif Date: Mon, 6 May 2024 08:29:58 -0500 Subject: [PATCH] fix(api): treat `col == None` or `col == ibis.NA` as `col.isnull()` (#9114) Co-authored-by: Phillip Cloud <417981+cpcloud@users.noreply.github.com> --- ibis/expr/types/core.py | 10 ++++++++++ ibis/expr/types/generic.py | 14 +++++++++----- ibis/tests/expr/test_value_exprs.py | 12 ++++++++++++ 3 files changed, 31 insertions(+), 5 deletions(-) diff --git a/ibis/expr/types/core.py b/ibis/expr/types/core.py index 1ab39d9d0725..a7fd748af5fd 100644 --- a/ibis/expr/types/core.py +++ b/ibis/expr/types/core.py @@ -737,3 +737,13 @@ def _binop(op_class: type[ops.Binary], left: ir.Value, right: ir.Value) -> ir.Va return NotImplemented else: return node.to_expr() + + +def _is_null_literal(value: Any) -> bool: + """Detect whether `value` will be treated by ibis as a null literal.""" + if value is None: + return True + if isinstance(value, Expr): + op = value.op() + return isinstance(op, ops.Literal) and op.value is None + return False diff --git a/ibis/expr/types/generic.py b/ibis/expr/types/generic.py index 4a554e17c273..367d6031a62e 100644 --- a/ibis/expr/types/generic.py +++ b/ibis/expr/types/generic.py @@ -13,7 +13,7 @@ from ibis.common.deferred import Deferred, _, deferrable from ibis.common.grounds import Singleton from ibis.expr.rewrites import rewrite_window_input -from ibis.expr.types.core import Expr, _binop, _FixedTextJupyterMixin +from ibis.expr.types.core import Expr, _binop, _FixedTextJupyterMixin, _is_null_literal from ibis.expr.types.pretty import to_rich from ibis.util import deprecated, warn_deprecated @@ -1160,13 +1160,17 @@ def __hash__(self) -> int: return super().__hash__() def __eq__(self, other: Value) -> ir.BooleanValue: - if other is None: - return _binop(ops.IdenticalTo, self, other) + if _is_null_literal(other): + return self.isnull() + elif _is_null_literal(self): + return other.isnull() return _binop(ops.Equals, self, other) def __ne__(self, other: Value) -> ir.BooleanValue: - if other is None: - return ~self.__eq__(other) + if _is_null_literal(other): + return self.notnull() + elif _is_null_literal(self): + return other.notnull() return _binop(ops.NotEquals, self, other) def __ge__(self, other: Value) -> ir.BooleanValue: diff --git a/ibis/tests/expr/test_value_exprs.py b/ibis/tests/expr/test_value_exprs.py index 5ded2434bfbf..ec8e3a224750 100644 --- a/ibis/tests/expr/test_value_exprs.py +++ b/ibis/tests/expr/test_value_exprs.py @@ -350,6 +350,18 @@ def test_notnull(table): assert isinstance(expr.op(), ops.NotNull) +@pytest.mark.parametrize( + "value", + [None, ibis.NA, ibis.literal(None, type="int32")], + ids=["none", "NA", "typed-null"], +) +def test_null_eq_and_ne(table, value): + assert (table.a == value).equals(table.a.isnull()) + assert (value == table.a).equals(table.a.isnull()) + assert (table.a != value).equals(table.a.notnull()) + assert (value != table.a).equals(table.a.notnull()) + + @pytest.mark.parametrize("column", ["e", "f"], ids=["float32", "double"]) def test_isnan_isinf_column(table, column): expr = table[column].isnan()