Repository: ibis-project/ibis

Issue:
bug: `CompileError: Multiple, unrelated CTEs` on union of complex Tables
### What happened?

```python
import ibis
from ibis import _
from ibis import examples as ex

t1 = ex.Aids2.fetch()
t2 = ex.Aids2.fetch()
cols = ["diag", "status"]
t1 = t1.select(*cols)
t2 = t2.select(*cols)
t1 = t1.mutate(diag = _.diag + 1)
t2 = t2.mutate(diag = _.diag + 1)
t1 = t1.mutate(diag=_.diag.cast("int32"))
t2 = t2.mutate(diag=_.diag.cast("int32"))
u = ibis.union(t1, t2)
u.execute()
```

Results in `CompileError: Multiple, unrelated CTEs found with the same name: 't0'`

If I skip one of these modifications, it does not error. If I re-order them, then it still errors.

This appears way too down in the guts of Ibis for me to want to tackle myself, sorry :)

### What version of ibis are you using?

main

### What backend(s) are you using, if any?

duckdb

### Relevant log output

_No response_

### Code of Conduct

- [X] I agree to follow this project's Code of Conduct


Discussion:
I looked into this. We're being too clever about alias generation.

Resolving patch:
diff --git a/ibis/backends/base/sql/compiler/translator.py b/ibis/backends/base/sql/compiler/translator.py
--- a/ibis/backends/base/sql/compiler/translator.py
+++ b/ibis/backends/base/sql/compiler/translator.py
@@ -27,6 +27,7 @@ def __init__(self, compiler, indent=2, parent=None, params=None):
         self.always_alias = True
         self.query = None
         self.params = params if params is not None else {}
+        self._alias_counter = getattr(parent, "_alias_counter", 0)
 
     def _compile_subquery(self, op):
         sub_ctx = self.subcontext()
@@ -73,11 +74,9 @@ def get_compiled_expr(self, node):
         return result
 
     def make_alias(self, node):
-        i = len(self.table_refs)
+        i = self._alias_counter
 
-        # Get total number of aliases up and down the tree at this point; if we
-        # find the table prior-aliased along the way, however, we reuse that
-        # alias
+        # check for existing tables that we're referencing from a parent context
         for ctx in itertools.islice(self._contexts(), 1, None):
             try:
                 alias = ctx.table_refs[node]
@@ -87,8 +86,7 @@ def make_alias(self, node):
                 self.set_ref(node, alias)
                 return
 
-            i += len(ctx.table_refs)
-
+        self._alias_counter += 1
         alias = f't{i:d}'
         self.set_ref(node, alias)
 


Test patch:
diff --git a/ibis/backends/impala/tests/snapshots/test_sql/test_join_key_name/out.sql b/ibis/backends/impala/tests/snapshots/test_sql/test_join_key_name/out.sql
--- a/ibis/backends/impala/tests/snapshots/test_sql/test_join_key_name/out.sql
+++ b/ibis/backends/impala/tests/snapshots/test_sql/test_join_key_name/out.sql
@@ -13,9 +13,9 @@ t1 AS (
   SELECT extract(t0.`odate`, 'year') AS `year`, count(1) AS `count`
   FROM t0
   WHERE t0.`o_totalprice` > (
-    SELECT avg(t4.`o_totalprice`) AS `mean`
-    FROM t0 t4
-    WHERE t4.`region` = t0.`region`
+    SELECT avg(t3.`o_totalprice`) AS `mean`
+    FROM t0 t3
+    WHERE t3.`region` = t0.`region`
   )
   GROUP BY 1
 ),
diff --git a/ibis/backends/impala/tests/snapshots/test_sql/test_nested_join_multiple_ctes/out.sql b/ibis/backends/impala/tests/snapshots/test_sql/test_nested_join_multiple_ctes/out.sql
--- a/ibis/backends/impala/tests/snapshots/test_sql/test_nested_join_multiple_ctes/out.sql
+++ b/ibis/backends/impala/tests/snapshots/test_sql/test_nested_join_multiple_ctes/out.sql
@@ -12,10 +12,10 @@ t1 AS (
   FROM ratings t3
 ),
 t2 AS (
-  SELECT t1.*, t4.`title`
+  SELECT t1.*, t3.`title`
   FROM t1
-    INNER JOIN movies t4
-      ON t1.`movieid` = t4.`movieid`
+    INNER JOIN movies t3
+      ON t1.`movieid` = t3.`movieid`
 )
 SELECT t2.*
 FROM t2
diff --git a/ibis/tests/sql/snapshots/test_compiler/test_table_drop_with_filter/out.sql b/ibis/tests/sql/snapshots/test_compiler/test_table_drop_with_filter/out.sql
--- a/ibis/tests/sql/snapshots/test_compiler/test_table_drop_with_filter/out.sql
+++ b/ibis/tests/sql/snapshots/test_compiler/test_table_drop_with_filter/out.sql
@@ -15,7 +15,7 @@ SELECT t3.*
 FROM (
   SELECT t2.`a`
   FROM t2
-    INNER JOIN s t5
-      ON t2.`b` = t5.`b`
+    INNER JOIN s t4
+      ON t2.`b` = t4.`b`
 ) t3
 WHERE t3.`a` < 1.0
\ No newline at end of file
diff --git a/ibis/tests/sql/snapshots/test_select_sql/test_complex_union/result.sql b/ibis/tests/sql/snapshots/test_select_sql/test_complex_union/result.sql
new file mode 100644
--- /dev/null
+++ b/ibis/tests/sql/snapshots/test_select_sql/test_complex_union/result.sql
@@ -0,0 +1,29 @@
+WITH t0 AS (
+  SELECT t6.`diag`, t6.`status`
+  FROM aids2_two t6
+),
+t1 AS (
+  SELECT t6.`diag`, t6.`status`
+  FROM aids2_one t6
+),
+t2 AS (
+  SELECT t0.`diag` + 1 AS `diag`, t0.`status`
+  FROM t0
+),
+t3 AS (
+  SELECT t1.`diag` + 1 AS `diag`, t1.`status`
+  FROM t1
+),
+t4 AS (
+  SELECT CAST(t2.`diag` AS int) AS `diag`, t2.`status`
+  FROM t2
+),
+t5 AS (
+  SELECT CAST(t3.`diag` AS int) AS `diag`, t3.`status`
+  FROM t3
+)
+SELECT *
+FROM t5
+UNION ALL
+SELECT *
+FROM t4
\ No newline at end of file
diff --git a/ibis/tests/sql/snapshots/test_select_sql/test_subquery_in_union/out.sql b/ibis/tests/sql/snapshots/test_select_sql/test_subquery_in_union/out.sql
--- a/ibis/tests/sql/snapshots/test_select_sql/test_subquery_in_union/out.sql
+++ b/ibis/tests/sql/snapshots/test_select_sql/test_subquery_in_union/out.sql
@@ -6,13 +6,13 @@ WITH t0 AS (
 t1 AS (
   SELECT t0.*
   FROM t0
-    INNER JOIN t0 t3
-      ON t0.`g` = t3.`g`
+    INNER JOIN t0 t2
+      ON t0.`g` = t2.`g`
 )
 SELECT *
 FROM t1
 UNION ALL
 SELECT t0.*
 FROM t0
-  INNER JOIN t0 t3
-    ON t0.`g` = t3.`g`
\ No newline at end of file
+  INNER JOIN t0 t2
+    ON t0.`g` = t2.`g`
\ No newline at end of file
diff --git a/ibis/tests/sql/snapshots/test_select_sql/test_subquery_used_for_self_join/out.sql b/ibis/tests/sql/snapshots/test_select_sql/test_subquery_used_for_self_join/out.sql
--- a/ibis/tests/sql/snapshots/test_select_sql/test_subquery_used_for_self_join/out.sql
+++ b/ibis/tests/sql/snapshots/test_select_sql/test_subquery_used_for_self_join/out.sql
@@ -6,10 +6,10 @@ WITH t0 AS (
 SELECT t0.`g`, max(t0.`total` - `total`) AS `metric`
 FROM (
   SELECT t0.`g` AS `g_x`, t0.`a` AS `a_x`, t0.`b` AS `b_x`,
-         t0.`total` AS `total_x`, t3.`g` AS `g_y`, t3.`a` AS `a_y`,
-         t3.`b` AS `b_y`, t3.`total` AS `total_y`
+         t0.`total` AS `total_x`, t2.`g` AS `g_y`, t2.`a` AS `a_y`,
+         t2.`b` AS `b_y`, t2.`total` AS `total_y`
   FROM t0
-    INNER JOIN t0 t3
-      ON t0.`a` = t3.`b`
+    INNER JOIN t0 t2
+      ON t0.`a` = t2.`b`
 ) t1
 GROUP BY 1
\ No newline at end of file
diff --git a/ibis/tests/sql/snapshots/test_sqlalchemy/test_gh_1045/out.sql b/ibis/tests/sql/snapshots/test_sqlalchemy/test_gh_1045/out.sql
--- a/ibis/tests/sql/snapshots/test_sqlalchemy/test_gh_1045/out.sql
+++ b/ibis/tests/sql/snapshots/test_sqlalchemy/test_gh_1045/out.sql
@@ -31,11 +31,11 @@ LEFT OUTER JOIN (
     t1.dt AS dt,
     t1.id3 AS id3,
     t1.t3_val2 AS t3_val2,
-    t5.id2a AS id2a,
-    t5.id2b AS id2b,
-    t5.val2 AS val2
+    t4.id2a AS id2a,
+    t4.id2b AS id2b,
+    t4.val2 AS val2
   FROM t1
-  JOIN test2 AS t5
-    ON t5.id2b = t1.id3
+  JOIN test2 AS t4
+    ON t4.id2b = t1.id3
 ) AS t3
   ON t2.t1_id1 = t3.id2a
\ No newline at end of file
diff --git a/ibis/tests/sql/snapshots/test_sqlalchemy/test_lower_projection_sort_key/out.sql b/ibis/tests/sql/snapshots/test_sqlalchemy/test_lower_projection_sort_key/out.sql
--- a/ibis/tests/sql/snapshots/test_sqlalchemy/test_lower_projection_sort_key/out.sql
+++ b/ibis/tests/sql/snapshots/test_sqlalchemy/test_lower_projection_sort_key/out.sql
@@ -9,10 +9,10 @@ WITH t0 AS (
   SELECT
     t0.foo_id AS foo_id,
     t0.total AS total,
-    t4.value1 AS value1
+    t3.value1 AS value1
   FROM t0
-  JOIN star2 AS t4
-    ON t0.foo_id = t4.foo_id
+  JOIN star2 AS t3
+    ON t0.foo_id = t3.foo_id
 )
 SELECT
   t2.foo_id,
diff --git a/ibis/tests/sql/snapshots/test_sqlalchemy/test_tpc_h11/out.sql b/ibis/tests/sql/snapshots/test_sqlalchemy/test_tpc_h11/out.sql
--- a/ibis/tests/sql/snapshots/test_sqlalchemy/test_tpc_h11/out.sql
+++ b/ibis/tests/sql/snapshots/test_sqlalchemy/test_tpc_h11/out.sql
@@ -26,14 +26,14 @@ FROM (
         anon_1.total
       FROM (
         SELECT
-          SUM(t3.ps_supplycost * t3.ps_availqty) AS total
-        FROM partsupp AS t3
-        JOIN supplier AS t4
-          ON t3.ps_suppkey = t4.s_suppkey
-        JOIN nation AS t5
-          ON t5.n_nationkey = t4.s_nationkey
+          SUM(t2.ps_supplycost * t2.ps_availqty) AS total
+        FROM partsupp AS t2
+        JOIN supplier AS t3
+          ON t2.ps_suppkey = t3.s_suppkey
+        JOIN nation AS t4
+          ON t4.n_nationkey = t3.s_nationkey
         WHERE
-          t5.n_name = 'GERMANY'
+          t4.n_name = 'GERMANY'
       ) AS anon_1
     ) * 0.0001
 ) AS t1
diff --git a/ibis/tests/sql/test_select_sql.py b/ibis/tests/sql/test_select_sql.py
--- a/ibis/tests/sql/test_select_sql.py
+++ b/ibis/tests/sql/test_select_sql.py
@@ -826,3 +826,21 @@ def test_incorrect_predicate_pushdown_with_literal(snapshot):
     t = ibis.table(dict(a="int"), name="t")
     expr = t.mutate(a=ibis.literal(1)).filter(lambda t: t.a > 1)
     snapshot.assert_match(to_sql(expr), "result.sql")
+
+
+def test_complex_union(snapshot):
+    def compute(t):
+        return (
+            t.select("diag", "status")
+            .mutate(diag=_.diag + 1)
+            .mutate(diag=_.diag.cast("int32"))
+        )
+
+    # schema subset from ibis.examples.Aids2
+    schema = ibis.schema(dict(diag="int64", status="string"))
+
+    t1 = compute(ibis.table(schema, name="aids2_one"))
+    t2 = compute(ibis.table(schema, name="aids2_two"))
+
+    u = ibis.union(t1, t2)
+    snapshot.assert_match(to_sql(u), "result.sql")