Repository: ibis-project/ibis

Issue:
bug: joins with join on the right hand side are not implemented
Paring down the example from #1045, this test fails:

```python
def test_1045(con):
    con.raw_sql("drop table if exists test1")
    con.raw_sql("drop table if exists test2")
    con.raw_sql("drop table if exists test3")

    con.raw_sql("create table test1 (id1 integer not null, val1 real)")
    for i in range(10):
        con.raw_sql(
            "insert into test1 values (1, 10), (2, 20), (3, 20), (2, 21)"
        )

    con.raw_sql(
        "create table test2 (id2a integer not null, id2b bigint, val2 double precision)"
    )
    for _ in range(10):
        con.raw_sql("insert into test2 values (1, 4, 15), (2, 5, 25)")

    con.raw_sql(
        "create table test3 (id3 text not null, val2 double precision, dt timestamp without time zone)"
    )
    for i in range(10):
        con.raw_sql(
            "insert into test3 values ('4', 15, '2016-07-17 19:55:03'), (2, 25, '2016-07-17 19:55:03')"
        )

    t1 = con.table('test1')
    t2 = con.table('test2')
    t3 = con.table('test3')

    t3 = t3[[c for c in t3.columns if c != "id3"]].mutate(
        id3=t3.id3.cast('int64')
    )

    t3 = t3[[c for c in t3.columns if c != "val2"]].mutate(t3_val2=t3.id3)
    from ibis.util import psql

    t4 = t3.join(t2, t2.id2b == t3.id3)

    t1 = t1[[t1[c].name(f"t1_{c}") for c in t1.columns]]

    t5 = t1.join(t4, t1.t1_id1 == t4.id2a)
```


Discussion:

Resolving patch:
diff --git a/ibis/backends/base/sql/compiler/query_builder.py b/ibis/backends/base/sql/compiler/query_builder.py
--- a/ibis/backends/base/sql/compiler/query_builder.py
+++ b/ibis/backends/base/sql/compiler/query_builder.py
@@ -61,21 +61,16 @@ def _walk_join_tree(self, op):
         if isinstance(left, ops.Join):
             self._walk_join_tree(left)
             self.join_tables.append(self._format_table(op.right))
-            self.join_types.append(jname)
-            self.join_predicates.append(op.predicates)
         elif isinstance(right, ops.Join):
-            # When rewrites are possible at the expression IR stage, we should
-            # do them. Otherwise subqueries might be necessary in some cases
-            # here
-            raise NotImplementedError(
-                'not allowing joins on right ' 'side yet'
-            )
+            self.join_tables.append(self._format_table(op.left))
+            self._walk_join_tree(right)
         else:
             # Both tables
             self.join_tables.append(self._format_table(op.left))
             self.join_tables.append(self._format_table(op.right))
-            self.join_types.append(jname)
-            self.join_predicates.append(op.predicates)
+
+        self.join_types.append(jname)
+        self.join_predicates.append(op.predicates)
 
     # Placeholder; revisit when supporting other databases
     _non_equijoin_supported = True
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
@@ -95,14 +95,25 @@ def blocks(self):
 
 
 def _make_distinct_join_predicates(left, right, predicates):
-    # see GH #667
-
-    # If left and right table have a common parent expression (e.g. they
-    # have different filters), must add a self-reference and make the
-    # appropriate substitution in the join predicates
+    import ibis.expr.analysis as L
 
     if left.equals(right):
+        # GH #667: If left and right table have a common parent expression,
+        # e.g. they have different filters, we need to add a self-reference and
+        # make the appropriate substitution in the join predicates
         right = right.view()
+    elif isinstance(right.op(), Join):
+        # for joins with joins on the right side we turn the right side into a
+        # view, otherwise the join tree is incorrectly flattened and tables on
+        # the right are incorrectly scoped
+        old = right
+        new = right = right.view()
+        predicates = [
+            L.sub_for(pred, [(old, new)])
+            if isinstance(pred, ir.Expr)
+            else pred
+            for pred in predicates
+        ]
 
     predicates = _clean_join_predicates(left, right, predicates)
     return left, right, predicates
@@ -113,9 +124,6 @@ def _clean_join_predicates(left, right, predicates):
 
     result = []
 
-    if not isinstance(predicates, (list, tuple)):
-        predicates = [predicates]
-
     for pred in predicates:
         if isinstance(pred, tuple):
             if len(pred) != 2:
@@ -162,7 +170,7 @@ class Join(TableNode):
 
     def __init__(self, left, right, predicates, **kwargs):
         left, right, predicates = _make_distinct_join_predicates(
-            left, right, predicates
+            left, right, util.promote_list(predicates)
         )
         super().__init__(
             left=left, right=right, predicates=predicates, **kwargs
@@ -241,7 +249,7 @@ class AsOfJoin(Join):
     tolerance = rlz.optional(rlz.interval)
 
     def __init__(self, left, right, by, predicates, **kwargs):
-        by = _clean_join_predicates(left, right, by)
+        by = _clean_join_predicates(left, right, util.promote_list(by))
         super().__init__(
             left=left, right=right, by=by, predicates=predicates, **kwargs
         )


Test patch:
diff --git a/ibis/tests/sql/test_sqlalchemy.py b/ibis/tests/sql/test_sqlalchemy.py
--- a/ibis/tests/sql/test_sqlalchemy.py
+++ b/ibis/tests/sql/test_sqlalchemy.py
@@ -901,3 +901,124 @@ def test_no_cross_join(person, visited, survey):
         ]
     ).select_from(from_)
     _check(expr, ex)
+
+
+@pytest.fixture
+def test1():
+    return ibis.table(name="test1", schema=dict(id1="int32", val1="float32"))
+
+
+@pytest.fixture
+def test2():
+    return ibis.table(
+        name="test2",
+        schema=ibis.schema(dict(id2a="int32", id2b="int64", val2="float64")),
+    )
+
+
+@pytest.fixture
+def test3():
+    return ibis.table(
+        name="test3",
+        schema=dict(id3="string", val2="float64", dt="timestamp"),
+    )
+
+
+def test_gh_1045(test1, test2, test3):
+    t1 = test1
+    t2 = test2
+    t3 = test3
+
+    t3 = t3[[c for c in t3.columns if c != "id3"]].mutate(
+        id3=t3.id3.cast('int64')
+    )
+
+    t3 = t3[[c for c in t3.columns if c != "val2"]].mutate(t3_val2=t3.id3)
+    t4 = t3.join(t2, t2.id2b == t3.id3)
+
+    t1 = t1[[t1[c].name(f"t1_{c}") for c in t1.columns]]
+
+    expr = t1.left_join(t4, t1.t1_id1 == t4.id2a)
+
+    test3 = sa.table(
+        "test3", sa.column("id3"), sa.column("val2"), sa.column("dt")
+    )
+    test2 = sa.table(
+        "test2", sa.column("id2a"), sa.column("id2b"), sa.column("val2")
+    )
+    test1 = sa.table("test1", sa.column("id1"), sa.column("val1"))
+
+    t2 = test1.alias("t2")
+    t0 = sa.select(
+        t2.c.id1.label("t1_id1"),
+        t2.c.val1.label("t1_val1"),
+    ).alias("t0")
+
+    t5 = test3.alias("t5")
+    t4 = sa.select(
+        t5.c.val2,
+        t5.c.dt,
+        sa.cast(t5.c.id3, sa.BigInteger()).label("id3"),
+    ).alias("t4")
+    t3 = test2.alias("t3")
+    t2 = sa.select(t4.c.dt, t4.c.id3, t4.c.id3.label("t3_val2")).alias("t2")
+    t1 = (
+        sa.select(
+            t2.c.dt,
+            t2.c.id3,
+            t2.c.t3_val2,
+            t3.c.id2a,
+            t3.c.id2b,
+            t3.c.val2,
+        )
+        .select_from(t2.join(t3, onclause=t3.c.id2b == t2.c.id3))
+        .alias("t1")
+    )
+
+    ex = sa.select(
+        t0.c.t1_id1,
+        t0.c.t1_val1,
+        t1.c.dt,
+        t1.c.id3,
+        t1.c.t3_val2,
+        t1.c.id2a,
+        t1.c.id2b,
+        t1.c.val2,
+    ).select_from(t0.join(t1, isouter=True, onclause=t0.c.t1_id1 == t1.c.id2a))
+
+    _check(expr, ex)
+
+
+def test_multi_join():
+    t1 = ibis.table(dict(x1="int64", y1="int64"), name="t1")
+    t2 = ibis.table(dict(x2="int64"), name="t2")
+    t3 = ibis.table(dict(x3="int64", y2="int64"), name="t3")
+    t4 = ibis.table(dict(x4="int64"), name="t4")
+
+    j1 = t1.join(t2, t1.x1 == t2.x2)
+    j2 = t3.join(t4, t3.x3 == t4.x4)
+    expr = j1.join(j2, j1.y1 == j2.y2)
+
+    t0 = sa.table("t1", sa.column("x1"), sa.column("y1")).alias("t0")
+    t1 = sa.table("t2", sa.column("x2")).alias("t1")
+
+    t3 = sa.table("t3", sa.column("x3"), sa.column("y2")).alias("t3")
+    t4 = sa.table("t4", sa.column("x4")).alias("t4")
+    t2 = (
+        sa.select(t3.c.x3, t3.c.y2, t4.c.x4)
+        .select_from(t3.join(t4, onclause=t3.c.x3 == t4.c.x4))
+        .alias("t2")
+    )
+    ex = sa.select(
+        t0.c.x1,
+        t0.c.y1,
+        t1.c.x2,
+        t2.c.x3,
+        t2.c.y2,
+        t2.c.x4,
+    ).select_from(
+        t0.join(t1, onclause=t0.c.x1 == t1.c.x2).join(
+            t2, onclause=t0.c.y1 == t2.c.y2
+        )
+    )
+    _check(expr, ex)