Skip to content

Commit

Permalink
fix(ir): implicitly convert None literals with dt.Null type to th…
Browse files Browse the repository at this point in the history
…e requested type during value coercion
  • Loading branch information
kszucs committed Dec 18, 2023
1 parent b1137f7 commit e64bb98
Show file tree
Hide file tree
Showing 5 changed files with 59 additions and 3 deletions.
10 changes: 8 additions & 2 deletions ibis/expr/operations/core.py
Original file line number Diff line number Diff line change
Expand Up @@ -68,13 +68,19 @@ def __coerce__(
) -> Self:
# note that S=Shape is unused here since the pattern will check the
# shape of the value expression after executing Value.__coerce__()
from ibis.expr.operations import Literal
from ibis.expr.operations.generic import NULL, Literal
from ibis.expr.types import Expr

if isinstance(value, Expr):
value = value.op()

if isinstance(value, Value):
return value
if value == NULL:
# treat the NULL literal the same as None to implicitly cast to
# the requested datatype if any
value = None
else:
return value

if T is dt.Integer:
dtype = dt.infer(int(value))
Expand Down
6 changes: 6 additions & 0 deletions ibis/expr/operations/generic.py
Original file line number Diff line number Diff line change
Expand Up @@ -195,6 +195,9 @@ def name(self):
return repr(self.value)


NULL = Literal(None, dt.null)


@public
class ScalarParameter(Scalar, Named):
_counter = itertools.count()
Expand Down Expand Up @@ -313,3 +316,6 @@ def shape(self):
def dtype(self):
exprs = [*self.results, self.default]
return rlz.highest_precedence_dtype(exprs)


public(NULL=NULL)
21 changes: 21 additions & 0 deletions ibis/expr/operations/tests/test_generic.py
Original file line number Diff line number Diff line change
Expand Up @@ -122,3 +122,24 @@ def test_error_message_when_constructing_literal(call, error, snapshot):
with pytest.raises(ValidationError) as exc:
call()
snapshot.assert_match(str(exc.value), f"{error}.txt")


def test_implicit_coercion_of_null_literal():
# GH #7775
NULL = ops.Literal(None, dt.null)

value = ops.Value.__coerce__(None, dt.Int8)
expected = ops.Literal(None, dt.int8)
assert value == expected

value = ops.Value.__coerce__(NULL, dt.Float64)
expected = ops.Literal(None, dt.float64)
assert value == expected


def test_NULL():
assert isinstance(ops.NULL, ops.Literal)
assert ops.NULL.value is None
assert ops.NULL.dtype is dt.null
assert ops.NULL == ops.Literal(None, dt.null)
assert ops.NULL is not ops.Literal(None, dt.int8)
23 changes: 23 additions & 0 deletions ibis/expr/tests/test_api.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
from __future__ import annotations

import operator
from datetime import datetime

import pandas as pd
Expand All @@ -9,6 +10,7 @@

import ibis
import ibis.expr.datatypes as dt
import ibis.expr.operations as ops
import ibis.expr.schema as sch
from ibis import _
from ibis.common.exceptions import IbisInputError, IntegrityError
Expand Down Expand Up @@ -124,3 +126,24 @@ def test_duplicate_columns_in_memtable_not_allowed():

with pytest.raises(IbisInputError, match="Duplicate column names"):
ibis.memtable(df)


@pytest.mark.parametrize(
"op",
[
operator.and_,
operator.or_,
operator.xor,
],
)
def test_implicit_coercion_of_null_literal(op):
# GH #7775
expr1 = op(ibis.literal(True), ibis.null())
expr2 = op(ibis.literal(True), None)

expected = expr1.op().__class__(
ops.Literal(True, dtype=dt.boolean), ops.Literal(None, dtype=dt.boolean)
)

assert expr1.op() == expected
assert expr2.op() == expected
2 changes: 1 addition & 1 deletion ibis/expr/types/generic.py
Original file line number Diff line number Diff line change
Expand Up @@ -1998,7 +1998,7 @@ class NullColumn(Column, NullValue):
@public
def null():
"""Create a NULL/NA scalar."""
return literal(None)
return ops.NULL.to_expr()


@public
Expand Down

0 comments on commit e64bb98

Please sign in to comment.