Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
10 changes: 8 additions & 2 deletions src/orbital/translation/optimizer.py
Original file line number Diff line number Diff line change
Expand Up @@ -328,8 +328,14 @@ def fold_operation(self, expr: ibis.Expr) -> ibis.Expr:
# No possible folding
return expr

def _debug(self, expr: ibis.Expr, show_args: bool = True) -> str:
"""Given an expression, return a string representation for debugging."""
def _debug( # pragma: no cover
self, expr: ibis.Expr, show_args: bool = True
) -> str:
"""Given an expression, return a string representation for debugging.

Only used on demand while developing the optimizer, so it is
intentionally left uncovered by the test suite.
"""
if isinstance(expr, Literal):
return repr(expr.value)
elif show_args is False:
Expand Down
175 changes: 175 additions & 0 deletions tests/test_optimizer.py
Original file line number Diff line number Diff line change
Expand Up @@ -147,3 +147,178 @@ def test_fold_operation_unary_not(self):
# not return the unreduced operation tree.
assert isinstance(result.op(), Literal)
assert result.op().value is False

def test_ensure_expr_passthrough_for_expr(self):
column = ibis.memtable({"a": [1.0]})["a"]
result = self.optimizer._ensure_expr(column)
assert result is column

def test_ensure_expr_wraps_literal_op_node(self):
op_node = ibis.literal(5).op()
result = self.optimizer._ensure_expr(op_node)
assert isinstance(result, ibis.Expr)
assert result.execute() == 5

def test_ensure_expr_raises_for_unsupported_type(self):
with pytest.raises(TypeError):
self.optimizer._ensure_expr(object())

def test_fold_contiguous_sum_disabled_returns_unchanged(self):
disabled = Optimizer(enabled=False)
a, b = ibis.literal(1), ibis.literal(2)
result = disabled.fold_contiguous_sum([a, b])
assert len(result) == 2
assert result[0] is a
assert result[1] is b

def test_fold_case_deferred_raises(self):
with pytest.raises(NotImplementedError):
self.optimizer.fold_case(ibis._.x)

def test_fold_case_disabled_returns_unchanged(self):
disabled = Optimizer(enabled=False)
expr = ibis.cases((ibis.literal(True), ibis.literal(1)), else_=ibis.literal(0))
result = disabled.fold_case(expr)
assert result is expr

def test_fold_case_all_results_same_value_folds_to_value(self):
# Every result and the default are literal 7, so the case can never
# produce anything else - it should collapse to a plain literal.
expr = ibis.cases(
(ibis.literal(True), ibis.literal(7)),
(ibis.literal(False), ibis.literal(7)),
else_=ibis.literal(7),
)
result = self.optimizer.fold_case(expr)
assert result.op().value == 7

def test_fold_case_literal_true_condition_returns_result(self):
expr = ibis.cases((ibis.literal(True), ibis.literal(1)), else_=ibis.literal(0))
result = self.optimizer.fold_case(expr)
assert result.op().value == 1

def test_fold_case_literal_false_condition_returns_default(self):
expr = ibis.cases((ibis.literal(False), ibis.literal(1)), else_=ibis.literal(0))
result = self.optimizer.fold_case(expr)
assert result.op().value == 0

def test_fold_case_boolean_ifelse_on_non_literal_condition_not_folded(self):
# Folding a single IF/ELSE returning 1 or 0 into a boolean cast is
# deliberately disabled (see the FIXME in fold_case: it doesn't work
# on postgresql), so a non-literal condition must come back
# unchanged rather than being rewritten.
table = ibis.memtable({"x": [1.0]})
condition = table["x"] > 0
expr = ibis.cases((condition, ibis.literal(1)), else_=ibis.literal(0))
result = self.optimizer.fold_case(expr)
assert result is expr

def test_fold_case_multiple_cases_returns_unchanged(self):
expr = ibis.cases(
(ibis.literal(True), ibis.literal("a")),
(ibis.literal(False), ibis.literal("b")),
else_=ibis.literal("c"),
)
result = self.optimizer.fold_case(expr)
assert result is expr

def test_fold_cast_disabled_returns_unchanged(self):
disabled = Optimizer(enabled=False)
expr = ibis.literal(5).cast("float64")
result = disabled.fold_cast(expr)
assert result is expr

def test_fold_cast_non_cast_expr_returns_unchanged(self):
# Ibis itself drops a cast to a column's own type, so fold_cast
# never even sees a Cast node here - it should just hand the
# expression back.
table = ibis.memtable({"a": [1.0]})
column = table["a"]
not_a_cast = column.cast("float64")
assert not isinstance(not_a_cast.op(), ibis.expr.operations.Cast)
result = self.optimizer.fold_cast(not_a_cast)
assert result is not_a_cast

def test_fold_cast_unsupported_literal_type_raises(self):
expr = ibis.literal(5).cast("date")
with pytest.raises(NotImplementedError):
self.optimizer.fold_cast(expr)

def test_fold_cast_already_target_type_unwraps_manual_cast(self):
# Build a Cast node directly (bypassing ibis's own .cast(), which
# already drops a same-type cast on its own) so fold_cast has to be
# the one to notice the arg is already the target type.
table = ibis.memtable({"a": [1.0]})
column = table["a"]
manual_cast = ibis.expr.operations.Cast(column.op(), to=column.type()).to_expr()
result = self.optimizer.fold_cast(manual_cast)
assert result.equals(column)

def test_fold_zeros_disabled_returns_unchanged(self):
disabled = Optimizer(enabled=False)
expr = ibis.literal(0) * ibis.literal(7)
result = disabled.fold_zeros(expr)
assert result is expr

def test_fold_zeros_multiply_left_zero(self):
expr = ibis.literal(0) * ibis.literal(7)
result = self.optimizer.fold_zeros(expr)
assert result.op().value == 0

def test_fold_zeros_multiply_right_zero(self):
expr = ibis.literal(7) * ibis.literal(0)
result = self.optimizer.fold_zeros(expr)
assert result.op().value == 0

def test_fold_zeros_add_left_zero(self):
table = ibis.memtable({"value": [1.0, 2.0]})
column = table["value"]
expr = ibis.literal(0) + column
result = self.optimizer.fold_zeros(expr)
assert result.equals(column)

def test_fold_zeros_add_right_zero(self):
table = ibis.memtable({"value": [1.0, 2.0]})
column = table["value"]
expr = column + ibis.literal(0)
result = self.optimizer.fold_zeros(expr)
assert result.equals(column)

def test_fold_zeros_no_zero_operand_returns_unchanged(self):
expr = ibis.literal(3) - ibis.literal(4)
result = self.optimizer.fold_zeros(expr)
assert result is expr

def test_fold_operation_disabled_returns_unchanged(self):
disabled = Optimizer(enabled=False)
expr = ibis.literal(2) + ibis.literal(3)
result = disabled.fold_operation(expr)
assert result is expr

def test_fold_operation_python_scalar_wrapped_as_literal(self):
# fold_operation can be handed a plain Python value when a previous
# fold already reduced a subtree to a scalar; it still has to come
# back out as an ibis expression.
result = self.optimizer.fold_operation(5)
assert isinstance(result, ibis.Expr)
assert result.op().value == 5

def test_fold_operation_mixed_literal_and_column_delegates_to_fold_zeros(self):
table = ibis.memtable({"value": [1.0, 2.0]})
column = table["value"]
expr = column * ibis.literal(0)
result = self.optimizer.fold_operation(expr)
assert result.op().value == 0

def test_fold_operation_binary_literal_folding(self):
expr = ibis.literal(2) + ibis.literal(3)
result = self.optimizer.fold_operation(expr)
assert isinstance(result.op(), Literal)
assert result.op().value == 5

def test_fold_operation_unfoldable_op_returns_unchanged(self):
# IsNull isn't in BINARY_OPS or UNARY_OPS, so even with an
# all-literal input there's nothing fold_operation can precompute.
expr = ibis.literal(5).isnull()
result = self.optimizer.fold_operation(expr)
assert result is expr
Loading