Repository: ibis-project/ibis

Issue:
bug: can't use deferred expression in join predicate
We don't support the use of deferred expressions in (at least) equality join predicates:

Reproducer:
```python
import ibis
from ibis import _ as C


t1 = ibis.table(
    [("c", "str"), ("d", "int64")],
    name="table0",
)
t2 = ibis.table(
    [("a", "str"), ("b", "int64")],
    name="table1",
)
t3 = ibis.table([("e", "str"), ("f", "int64")], name="table2")

t1.join(t2, t1.c == t2.a).join(t3, C.c == t3.e)
```

Traceback:
```python
---------------------------------------------------------------------------
NotImplementedError                       Traceback (most recent call last)
Cell In [15], line 1
----> 1 t1.join(t2, t1.c == t2.a).join(t3, C.c == t3.e)

File ~/github.com/ibis-project/ibis/ibis/expr/types/relations.py:1113, in Table.join(left, right, predicates, how, suffixes)
   1099 _join_classes = {
   1100     'inner': ops.InnerJoin,
   1101     'left': ops.LeftJoin,
   (...)
   1109     'cross': ops.CrossJoin,
   1110 }
   1112 klass = _join_classes[how.lower()]
-> 1113 expr = klass(left, right, predicates).to_expr()
   1115 # semi/anti join only give access to the left table's fields, so
   1116 # there's never overlap
   1117 if how in ("semi", "anti"):

File ~/github.com/ibis-project/ibis/ibis/common/grounds.py:26, in BaseMeta.__call__(cls, *args, **kwargs)
     25 def __call__(cls, *args, **kwargs) -> Base:
---> 26     return cls.__create__(*args, **kwargs)

File ~/github.com/ibis-project/ibis/ibis/common/grounds.py:106, in Annotable.__create__(cls, *args, **kwargs)
    102 @classmethod
    103 def __create__(cls, *args, **kwargs) -> Annotable:
    104     # construct the instance by passing the validated keyword arguments
    105     kwargs = cls.__signature__.validate(*args, **kwargs)
--> 106     return super().__create__(**kwargs)

File ~/github.com/ibis-project/ibis/ibis/common/grounds.py:35, in Base.__create__(cls, *args, **kwargs)
     33 @classmethod
     34 def __create__(cls, *args, **kwargs) -> Base:
---> 35     return type.__call__(cls, *args, **kwargs)

File ~/github.com/ibis-project/ibis/ibis/expr/operations/relations.py:172, in Join.__init__(self, left, right, predicates, **kwargs)
    166     new = right = ops.SelfReference(right)
    167     predicates = [
    168         L.sub_for(pred, {old: new}) if isinstance(pred, ops.Node) else pred
    169         for pred in predicates
    170     ]
--> 172 predicates = _clean_join_predicates(left, right, predicates)
    174 super().__init__(left=left, right=right, predicates=predicates, **kwargs)

File ~/github.com/ibis-project/ibis/ibis/expr/operations/relations.py:113, in _clean_join_predicates(left, right, predicates)
    111     pred = pred.to_expr()
    112 elif not isinstance(pred, ir.Expr):
--> 113     raise NotImplementedError
    115 if not isinstance(pred, ir.BooleanColumn):
    116     raise com.ExpressionError('Join predicate must be comparison')

NotImplementedError: 
```


Discussion:
We're doubling down on this, such that `_` (your `C`) should always refer to the left-hand side of the expression ("self").  We'll fix this.  Thanks for bringing it to our attention.

Resolving patch:
diff --git a/ibis/expr/operations/relations.py b/ibis/expr/operations/relations.py
--- a/ibis/expr/operations/relations.py
+++ b/ibis/expr/operations/relations.py
@@ -13,6 +13,7 @@
 import ibis.expr.types as ir
 import ibis.util as util
 from ibis.common.annotations import attribute
+from ibis.expr.deferred import Deferred
 from ibis.expr.operations.core import Named, Node, Value
 from ibis.expr.operations.generic import TableColumn
 from ibis.expr.operations.logical import Equals, ExistsSubquery, NotExistsSubquery
@@ -109,6 +110,9 @@ def _clean_join_predicates(left, right, predicates):
             pred = left.to_expr()[pred] == right.to_expr()[pred]
         elif isinstance(pred, Value):
             pred = pred.to_expr()
+        elif isinstance(pred, Deferred):
+            # resolve deferred expressions on the left table
+            pred = pred.resolve(left.to_expr())
         elif not isinstance(pred, ir.Expr):
             raise NotImplementedError
 


Test patch:
diff --git a/ibis/tests/expr/test_table.py b/ibis/tests/expr/test_table.py
--- a/ibis/tests/expr/test_table.py
+++ b/ibis/tests/expr/test_table.py
@@ -794,6 +794,14 @@ def test_join_no_predicate_list(con):
     assert_equal(joined, expected)
 
 
+def test_join_deferred(con):
+    region = con.table("tpch_region")
+    nation = con.table("tpch_nation")
+    res = region.join(nation, _.r_regionkey == nation.n_regionkey)
+    exp = region.join(nation, region.r_regionkey == nation.n_regionkey)
+    assert_equal(res, exp)
+
+
 def test_asof_join():
     left = ibis.table([('time', 'int32'), ('value', 'double')])
     right = ibis.table([('time', 'int32'), ('value2', 'double')])