Repository: ibis-project/ibis

Issue:
bug: can't use `ArrayValue.filter()` and friends with Deferreds
### What happened?

```python
import ibis
from ibis import _

ibis.options.interactive = True

t = ibis.memtable({"x": [[1, 2, 3]]})
t.mutate(t.x.filter(lambda y: y == 2))  # works
t.mutate(t.x.map(lambda y: y + 1))  # works
t.mutate(_.x.filter(lambda y: y == 2))  # error
t.mutate(_.x.map(lambda y: y + 1))  # error
```

See log below. 

### What version of ibis are you using?

d6a2f0922ea92b0e03a0431bf9011e7103565061 (master as of this posting)

### What backend(s) are you using, if any?

NA

### Relevant log output

```sh
---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
/Users/nc/code/ibis/.scratch/deferred_filter.ipynb Cell 1 line 1
      8 t = ibis.memtable({"x": [[1, 2, 3]]})
      9 t.mutate(t.x.filter(lambda y: y == 2))  # works
---> 10 t.mutate(_.x.filter(lambda y: y == 2))  # error

File ~/code/ibis/ibis/expr/types/relations.py:1813, in Table.mutate(self, exprs, **mutations)
   1802 exprs = [] if exprs is None else util.promote_list(exprs)
   1803 exprs = itertools.chain(
   1804     itertools.chain.from_iterable(
   1805         util.promote_list(_ensure_expr(self, expr)) for expr in exprs
   (...)
   1811     ),
   1812 )
-> 1813 mutation_exprs = an.get_mutation_exprs(list(exprs), self)
   1814 return self.select(mutation_exprs)

File ~/code/ibis/ibis/expr/types/relations.py:1805, in <genexpr>(.0)
   1800 import ibis.expr.analysis as an
   1802 exprs = [] if exprs is None else util.promote_list(exprs)
   1803 exprs = itertools.chain(
   1804     itertools.chain.from_iterable(
-> 1805         util.promote_list(_ensure_expr(self, expr)) for expr in exprs
   1806     ),
   1807     (
   1808         e.name(name)
   1809         for name, expr in mutations.items()
   1810         for e in util.promote_list(_ensure_expr(self, expr))
   1811     ),
   1812 )
   1813 mutation_exprs = an.get_mutation_exprs(list(exprs), self)
   1814 return self.select(mutation_exprs)

File ~/code/ibis/ibis/expr/types/relations.py:50, in _ensure_expr(table, expr)
     48     return expr(table)
     49 elif isinstance(expr, Deferred):
---> 50     return expr.resolve(table)
     51 elif isinstance(expr, Selector):
     52     return expr.expand(table)

File ~/code/ibis/ibis/common/deferred.py:87, in Deferred.resolve(self, _, **kwargs)
     85 def resolve(self, _=None, **kwargs):
     86     context = {"_": _, **kwargs}
---> 87     return self._resolver.resolve(context)

File ~/code/ibis/ibis/common/deferred.py:391, in Call.resolve(self, context)
    389 def resolve(self, context):
    390     func = self.func.resolve(context)
--> 391     args = tuple(arg.resolve(context) for arg in self.args)
    392     kwargs = {k: v.resolve(context) for k, v in self.kwargs.items()}
    393     return func(*args, **kwargs)

File ~/code/ibis/ibis/common/deferred.py:391, in <genexpr>(.0)
    389 def resolve(self, context):
    390     func = self.func.resolve(context)
--> 391     args = tuple(arg.resolve(context) for arg in self.args)
    392     kwargs = {k: v.resolve(context) for k, v in self.kwargs.items()}
    393     return func(*args, **kwargs)

File ~/code/ibis/ibis/common/deferred.py:313, in Factory.resolve(self, context)
    312 def resolve(self, context):
--> 313     return self.func(**context)

TypeError: <lambda>() got an unexpected keyword argument '_'
```


### Code of Conduct

- [X] I agree to follow this project's Code of Conduct


Discussion:
@kszucs Let's make sure to add a test for this in #7580 

Resolving patch:
diff --git a/ibis/backends/clickhouse/compiler/core.py b/ibis/backends/clickhouse/compiler/core.py
--- a/ibis/backends/clickhouse/compiler/core.py
+++ b/ibis/backends/clickhouse/compiler/core.py
@@ -28,7 +28,8 @@
 from ibis.backends.clickhouse.compiler.relations import translate_rel
 from ibis.backends.clickhouse.compiler.values import translate_val
 from ibis.common.deferred import _
-from ibis.expr.analysis import c, find_first_base_table, p, x, y
+from ibis.common.patterns import replace
+from ibis.expr.analysis import c, find_first_base_table, p, x
 from ibis.expr.rewrites import rewrite_dropna, rewrite_fillna, rewrite_sample
 
 if TYPE_CHECKING:
@@ -42,6 +43,14 @@ def _translate_node(node, **kwargs):
     return translate_rel(node, **kwargs)
 
 
+@replace(ops.InColumn)
+def replace_in_column_with_table_array_view(_):
+    # replace the right side of InColumn into a scalar subquery for sql backends
+    base = find_first_base_table(_.options)
+    options = ops.TableArrayView(ops.Selection(table=base, selections=(_.options,)))
+    return _.copy(options=options)
+
+
 def translate(op: ops.TableNode, params: Mapping[ir.Value, Any]) -> sg.exp.Expression:
     """Translate an ibis operation to a sqlglot expression.
 
@@ -88,14 +97,6 @@ def fn(node, _, **kwargs):
         lambda _, x: ops.Literal(value=params[_], dtype=x)
     )
 
-    # replace the right side of InColumn into a scalar subquery for sql
-    # backends
-    replace_in_column_with_table_array_view = p.InColumn(options=y) >> _.copy(
-        options=c.TableArrayView(
-            c.Selection(table=lambda _, y: find_first_base_table(y), selections=(y,))
-        ),
-    )
-
     # replace any checks against an empty right side of the IN operation with
     # `False`
     replace_empty_in_values_with_false = p.InValues(options=()) >> c.Literal(
diff --git a/ibis/common/deferred.py b/ibis/common/deferred.py
--- a/ibis/common/deferred.py
+++ b/ibis/common/deferred.py
@@ -527,10 +527,6 @@ def resolver(obj):
             return Just(obj)
         else:
             return Sequence(obj)
-    elif isinstance(obj, type):
-        return Just(obj)
-    elif callable(obj):
-        return Factory(obj)
     else:
         # the object is used as a constant value
         return Just(obj)
diff --git a/ibis/common/patterns.py b/ibis/common/patterns.py
--- a/ibis/common/patterns.py
+++ b/ibis/common/patterns.py
@@ -25,6 +25,7 @@
 from ibis.common.collections import FrozenDict, RewindableIterator, frozendict
 from ibis.common.deferred import (
     Deferred,
+    Factory,
     Resolver,
     Variable,
     _,  # noqa: F401
@@ -46,6 +47,13 @@
 T_co = TypeVar("T_co", covariant=True)
 
 
+def as_resolver(obj):
+    if callable(obj) and not isinstance(obj, Deferred):
+        return Factory(obj)
+    else:
+        return resolver(obj)
+
+
 class NoMatch(metaclass=Sentinel):
     """Marker to indicate that a pattern didn't match."""
 
@@ -331,7 +339,7 @@ class Capture(Slotted, Pattern):
 
     def __init__(self, key, pat=_any):
         if isinstance(key, (Deferred, Resolver)):
-            key = resolver(key)
+            key = as_resolver(key)
             if isinstance(key, Variable):
                 key = key.name
             else:
@@ -353,25 +361,25 @@ class Replace(Slotted, Pattern):
     ----------
     matcher
         The pattern to match against.
-    resolver
+    replacer
         The deferred to use as a replacement.
     """
 
-    __slots__ = ("pattern", "resolver")
-    pattern: Pattern
-    resolver: Resolver
+    __slots__ = ("matcher", "replacer")
+    matcher: Pattern
+    replacer: Resolver
 
     def __init__(self, matcher, replacer):
-        super().__init__(pattern=pattern(matcher), resolver=resolver(replacer))
+        super().__init__(matcher=pattern(matcher), replacer=as_resolver(replacer))
 
     def match(self, value, context):
-        value = self.pattern.match(value, context)
+        value = self.matcher.match(value, context)
         if value is NoMatch:
             return NoMatch
         # use the `_` reserved variable to record the value being replaced
         # in the context, so that it can be used in the replacer pattern
         context["_"] = value
-        return self.resolver.resolve(context)
+        return self.replacer.resolve(context)
 
 
 def replace(matcher):
@@ -424,7 +432,7 @@ class DeferredCheck(Slotted, Pattern):
     resolver: Resolver
 
     def __init__(self, obj):
-        super().__init__(resolver=resolver(obj))
+        super().__init__(resolver=as_resolver(obj))
 
     def describe(self, plural=False):
         if plural:
@@ -505,7 +513,7 @@ class DeferredEqualTo(Slotted, Pattern):
     resolver: Resolver
 
     def __init__(self, obj):
-        super().__init__(resolver=resolver(obj))
+        super().__init__(resolver=as_resolver(obj))
 
     def match(self, value, context):
         context["_"] = value


Test patch:
diff --git a/ibis/common/tests/test_deferred.py b/ibis/common/tests/test_deferred.py
--- a/ibis/common/tests/test_deferred.py
+++ b/ibis/common/tests/test_deferred.py
@@ -145,14 +145,14 @@ def test_builder():
     class MyClass:
         pass
 
-    def fn(x, ctx):
+    def fn(x):
         return x + 1
 
     assert resolver(1) == Just(1)
     assert resolver(Just(1)) == Just(1)
     assert resolver(Just(Just(1))) == Just(1)
     assert resolver(MyClass) == Just(MyClass)
-    assert resolver(fn) == Factory(fn)
+    assert resolver(fn) == Just(fn)
     assert resolver(()) == Sequence(())
     assert resolver((1, 2, _)) == Sequence((Just(1), Just(2), _))
     assert resolver({}) == Mapping({})
diff --git a/ibis/tests/expr/test_value_exprs.py b/ibis/tests/expr/test_value_exprs.py
--- a/ibis/tests/expr/test_value_exprs.py
+++ b/ibis/tests/expr/test_value_exprs.py
@@ -1691,3 +1691,12 @@ def test_sample():
     assert op.fraction == 0.5
     assert op.method == "block"
     assert op.seed == 1234
+
+
+def test_deferred_doesnt_convert_callables():
+    t = ibis.table([("a", "int64"), ("b", "string")])
+    expr = t.mutate(b=_.b.split(",").filter(lambda pp: ~pp.isin(("word1", "word2"))))
+    expected = t.mutate(
+        b=t.b.split(",").filter(lambda pp: ~pp.isin(("word1", "word2")))
+    )
+    assert expr.equals(expected)