Repository: tobymao/sqlglot

Issue:
Transform exp.Explode and exp.Posexplode into exp.Unnest
In some languages (at least Spark SQL), `select explode(array_col) as a` will unravel the `array_col` such that each element is on one line. If this is selected along with other columns, this is semantically the same as a cross join.

Some languages (not sure which, but at least Presto and Trino) have support for this as `select a from unnest(array_col) as unnested_alias.b`. If there's a select from a table, it will be a cross join. 

Examples of what we want

```
select explode(array_col)
-> select col from unnest(array_col) as some_alias(col)

select explode(array_col) as alias
-> select alias from unnest(array_col) as some_alias(alias)

select posexplode(array_col)
-> select pos, col from unnest(array_col) with ordinality as some_alias(col, pos)

select a, posexplode(array_col) from table
-> select a, pos, col cross join unnest(array_col) with ordinality as some_alias(col, pos)
```

There is support for this going in the opposite direction, cross join unnest to lateral view explode. Used in presto -> hive:
https://github.com/tobymao/sqlglot/blob/e40cb6312f8492d51fed5df454efca39bd517e39/sqlglot/transforms.py#L140-L161 
(Frankly, I'm not sure of the difference in just using explode and using lateral view explode.)

With help from @GeorgeSittas I've [added support for this](https://github.com/duneanalytics/harmonizer/blob/e022b92fe0bd75ceacea25d6aa2124a66f3e6e85/dune/harmonizer/dialects/dunesql.py#L8-L68) in [duneanalytics/harmonizer](https://github.com/duneanalytics/harmonizer) by adding it as a preprocessing step. Test cases are [here](https://github.com/duneanalytics/harmonizer/blob/e022b92fe0bd75ceacea25d6aa2124a66f3e6e85/tests/test_dialect.py#L42-L116).

Would love to harden this and get it in upstream, so I'm planning to create a PR for it.


Discussion:
Awesome, feel free to create a PR and we'll be happy to help improve the current implementation and get it upstream!

Resolving patch:
diff --git a/sqlglot/dialects/presto.py b/sqlglot/dialects/presto.py
--- a/sqlglot/dialects/presto.py
+++ b/sqlglot/dialects/presto.py
@@ -265,7 +265,6 @@ class Generator(generator.Generator):
         TRANSFORMS = {
             **generator.Generator.TRANSFORMS,  # type: ignore
             **transforms.UNALIAS_GROUP,  # type: ignore
-            **transforms.ELIMINATE_QUALIFY,  # type: ignore
             exp.ApproxDistinct: _approx_distinct_sql,
             exp.Array: lambda self, e: f"ARRAY[{self.expressions(e, flat=True)}]",
             exp.ArrayConcat: rename_func("CONCAT"),
@@ -303,6 +302,9 @@ class Generator(generator.Generator):
             exp.ApproxQuantile: rename_func("APPROX_PERCENTILE"),
             exp.SafeDivide: no_safe_divide_sql,
             exp.Schema: _schema_sql,
+            exp.Select: transforms.preprocess(
+                [transforms.eliminate_qualify, transforms.explode_to_unnest]
+            ),
             exp.SortArray: _no_sort_array,
             exp.StrPosition: rename_func("STRPOS"),
             exp.StrToDate: lambda self, e: f"CAST({_str_to_time_sql(self, e)} AS DATE)",
diff --git a/sqlglot/expressions.py b/sqlglot/expressions.py
--- a/sqlglot/expressions.py
+++ b/sqlglot/expressions.py
@@ -4713,6 +4713,8 @@ def alias_(
 
     if table:
         table_alias = TableAlias(this=alias)
+
+        exp = exp.copy()
         exp.set("alias", table_alias)
 
         if not isinstance(table, bool):
diff --git a/sqlglot/transforms.py b/sqlglot/transforms.py
--- a/sqlglot/transforms.py
+++ b/sqlglot/transforms.py
@@ -138,7 +138,7 @@ def remove_precision_parameterized_types(expression: exp.Expression) -> exp.Expr
 
 
 def unnest_to_explode(expression: exp.Expression) -> exp.Expression:
-    """Convert cross join unnest into lateral view explode. Used in presto -> hive"""
+    """Convert cross join unnest into lateral view explode (used in presto -> hive)."""
     if isinstance(expression, exp.Select):
         for join in expression.args.get("joins") or []:
             unnest = join.this
@@ -161,6 +161,67 @@ def unnest_to_explode(expression: exp.Expression) -> exp.Expression:
     return expression
 
 
+def explode_to_unnest(expression: exp.Expression) -> exp.Expression:
+    """Convert explode/posexplode into unnest (used in hive -> presto)."""
+    if isinstance(expression, exp.Select):
+        from sqlglot.optimizer.scope import build_scope
+
+        taken_select_names = set(expression.named_selects)
+        taken_source_names = set(build_scope(expression).selected_sources)
+
+        for select in expression.selects:
+            to_replace = select
+
+            pos_alias = ""
+            explode_alias = ""
+
+            if isinstance(select, exp.Alias):
+                explode_alias = select.alias
+                select = select.this
+            elif isinstance(select, exp.Aliases):
+                pos_alias = select.aliases[0].name
+                explode_alias = select.aliases[1].name
+                select = select.this
+
+            if isinstance(select, (exp.Explode, exp.Posexplode)):
+                is_posexplode = isinstance(select, exp.Posexplode)
+
+                explode_arg = select.this
+                unnest = exp.Unnest(expressions=[explode_arg.copy()], ordinality=is_posexplode)
+
+                # This ensures that we won't use [POS]EXPLODE's argument as a new selection
+                if isinstance(explode_arg, exp.Column):
+                    taken_select_names.add(explode_arg.output_name)
+
+                unnest_source_alias = find_new_name(taken_source_names, "_u")
+                taken_source_names.add(unnest_source_alias)
+
+                if not explode_alias:
+                    explode_alias = find_new_name(taken_select_names, "col")
+                    taken_select_names.add(explode_alias)
+
+                    if is_posexplode:
+                        pos_alias = find_new_name(taken_select_names, "pos")
+                        taken_select_names.add(pos_alias)
+
+                if is_posexplode:
+                    column_names = [explode_alias, pos_alias]
+                    to_replace.pop()
+                    expression.select(pos_alias, explode_alias, copy=False)
+                else:
+                    column_names = [explode_alias]
+                    to_replace.replace(exp.column(explode_alias))
+
+                unnest = exp.alias_(unnest, unnest_source_alias, table=column_names)
+
+                if not expression.args.get("from"):
+                    expression.from_(unnest, copy=False)
+                else:
+                    expression.join(unnest, join_type="CROSS", copy=False)
+
+    return expression
+
+
 def remove_target_from_merge(expression: exp.Expression) -> exp.Expression:
     """Remove table refs from columns in when statements."""
     if isinstance(expression, exp.Merge):


Test patch:
diff --git a/tests/dialects/test_presto.py b/tests/dialects/test_presto.py
--- a/tests/dialects/test_presto.py
+++ b/tests/dialects/test_presto.py
@@ -676,3 +676,41 @@ def test_json(self):
                 "presto": "SELECT CAST(ARRAY[1, 23, 456] AS JSON)",
             },
         )
+
+    def test_explode_to_unnest(self):
+        self.validate_all(
+            "SELECT col FROM tbl CROSS JOIN UNNEST(x) AS _u(col)",
+            read={"spark": "SELECT EXPLODE(x) FROM tbl"},
+        )
+        self.validate_all(
+            "SELECT col_2 FROM _u CROSS JOIN UNNEST(col) AS _u_2(col_2)",
+            read={"spark": "SELECT EXPLODE(col) FROM _u"},
+        )
+        self.validate_all(
+            "SELECT exploded FROM schema.tbl CROSS JOIN UNNEST(col) AS _u(exploded)",
+            read={"spark": "SELECT EXPLODE(col) AS exploded FROM schema.tbl"},
+        )
+        self.validate_all(
+            "SELECT col FROM UNNEST(SEQUENCE(1, 2)) AS _u(col)",
+            read={"spark": "SELECT EXPLODE(SEQUENCE(1, 2))"},
+        )
+        self.validate_all(
+            "SELECT col FROM tbl AS t CROSS JOIN UNNEST(t.c) AS _u(col)",
+            read={"spark": "SELECT EXPLODE(t.c) FROM tbl t"},
+        )
+        self.validate_all(
+            "SELECT pos, col FROM UNNEST(SEQUENCE(2, 3)) WITH ORDINALITY AS _u(col, pos)",
+            read={"spark": "SELECT POSEXPLODE(SEQUENCE(2, 3))"},
+        )
+        self.validate_all(
+            "SELECT pos, col FROM tbl CROSS JOIN UNNEST(SEQUENCE(2, 3)) WITH ORDINALITY AS _u(col, pos)",
+            read={"spark": "SELECT POSEXPLODE(SEQUENCE(2, 3)) FROM tbl"},
+        )
+        self.validate_all(
+            "SELECT pos, col FROM tbl AS t CROSS JOIN UNNEST(t.c) WITH ORDINALITY AS _u(col, pos)",
+            read={"spark": "SELECT POSEXPLODE(t.c) FROM tbl t"},
+        )
+        self.validate_all(
+            "SELECT col, pos, pos_2, col_2 FROM _u CROSS JOIN UNNEST(SEQUENCE(2, 3)) WITH ORDINALITY AS _u_2(col_2, pos_2)",
+            read={"spark": "SELECT col, pos, POSEXPLODE(SEQUENCE(2, 3)) FROM _u"},
+        )