This is an automated email from the ASF dual-hosted git repository.

jhyde pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/calcite.git

commit 8346167252bcb90cd64ced9e3a5c893de28f88ed
Author: Julian Hyde <[email protected]>
AuthorDate: Tue Aug 3 21:36:20 2021 -0700

    [CALCITE-4485] JDBC adapter generates invalid SQL when one of the joins is 
"INNER JOIN ... ON TRUE"
    
    If the join tree is a mixture of cross-joins and outer joins,
    RelToSqlConverter wrongly generates comma-join syntax.
    Consider the (pseudo) RelNode tree
    
        CrossJoin(a, LeftJoin(b, CrossJoin(c, d)))
    
    Before this bug was fixed, RelToSqlConvert would generate
    
        FROM a, b LEFT JOIN c, d
    
    Because LEFT JOIN has higher precedence than ',', this is parsed to
    
        LeftJoin(CrossJoin(a, b), CrossJoin(c, d))
    
    which is incorrect. The fix is to only generate comma-join syntax
    if all joins are cross-joins. In this case, we will generate
    (pseudo SQL)
    
      FROM a CROSS JOIN b LEFT JOIN c CROSS JOIN d
    
    which is safe.
---
 .../calcite/rel/rel2sql/RelToSqlConverter.java     | 100 +++++++++++++--
 .../calcite/rel/rel2sql/RelToSqlConverterTest.java | 140 ++++++++++++++++++++-
 2 files changed, 229 insertions(+), 11 deletions(-)

diff --git 
a/core/src/main/java/org/apache/calcite/rel/rel2sql/RelToSqlConverter.java 
b/core/src/main/java/org/apache/calcite/rel/rel2sql/RelToSqlConverter.java
index c9fb0eb..4377e84 100644
--- a/core/src/main/java/org/apache/calcite/rel/rel2sql/RelToSqlConverter.java
+++ b/core/src/main/java/org/apache/calcite/rel/rel2sql/RelToSqlConverter.java
@@ -217,16 +217,22 @@ public class RelToSqlConverter extends SqlImplementor
     final Context leftContext = leftResult.qualifiedContext();
     final Context rightContext = rightResult.qualifiedContext();
     final SqlNode sqlCondition;
-    SqlLiteral condType = JoinConditionType.ON.symbol(POS);
+    final JoinConditionType condType;
     JoinType joinType = joinType(e.getJoinType());
-    if (isCrossJoin(e)) {
+    if (joinType == JoinType.INNER
+        && e.getCondition().isAlwaysTrue()) {
+      if (isCommaJoin(e)) {
+        joinType = JoinType.COMMA;
+      } else {
+        joinType = JoinType.CROSS;
+      }
       sqlCondition = null;
-      joinType = dialect.emulateJoinTypeForCrossJoin();
-      condType = JoinConditionType.NONE.symbol(POS);
+      condType = JoinConditionType.NONE;
     } else {
       sqlCondition =
           convertConditionToSqlNode(e.getCondition(), leftContext,
               rightContext);
+      condType = JoinConditionType.ON;
     }
     SqlNode join =
         new SqlJoin(POS,
@@ -234,7 +240,7 @@ public class RelToSqlConverter extends SqlImplementor
             SqlLiteral.createBoolean(false, POS),
             joinType.symbol(POS),
             rightResult.asFrom(),
-            condType,
+            condType.symbol(POS),
             sqlCondition);
     return result(join, leftResult, rightResult);
   }
@@ -289,8 +295,88 @@ public class RelToSqlConverter extends SqlImplementor
     return result(resultNode, leftResult, rightResult);
   }
 
-  private static boolean isCrossJoin(final Join e) {
-    return e.getJoinType() == JoinRelType.INNER && 
e.getCondition().isAlwaysTrue();
+  /** Returns whether this join should be unparsed as a {@link JoinType#COMMA}.
+   *
+   * <p>Comma-join is one possible syntax for {@code CROSS JOIN ... ON TRUE},
+   * supported on most but not all databases
+   * (see {@link SqlDialect#emulateJoinTypeForCrossJoin()}).
+   *
+   * <p>For example, the following queries are equivalent:
+   *
+   * <pre>{@code
+   * // Comma join
+   * SELECT *
+   * FROM Emp, Dept
+   *
+   * // Cross join
+   * SELECT *
+   * FROM Emp CROSS JOIN Dept
+   *
+   * // Inner join
+   * SELECT *
+   * FROM Emp INNER JOIN Dept ON TRUE
+   * }</pre>
+   *
+   * <p>Examples:
+   * <ul>
+   *   <li>{@code FROM (x CROSS JOIN y ON TRUE) CROSS JOIN z ON TRUE}
+   *   is a comma join, because all joins are comma joins;
+   *   <li>{@code FROM (x CROSS JOIN y ON TRUE) CROSS JOIN z ON TRUE}
+   *   would not be a comma join when run on Apache Spark, because Spark does
+   *   not support comma join;
+   *   <li>{@code FROM (x CROSS JOIN y ON TRUE) LEFT JOIN z ON TRUE}
+   *   is not comma join because one of the joins is not INNER;
+   *   <li>{@code FROM (x CROSS JOIN y ON c) CROSS JOIN z ON TRUE}
+   *   is not a comma join because one of the joins is ON TRUE.
+   * </ul>
+   */
+  private boolean isCommaJoin(Join join) {
+    if (!join.getCondition().isAlwaysTrue()) {
+      return false;
+    }
+    if (dialect.emulateJoinTypeForCrossJoin() != JoinType.COMMA) {
+      return false;
+    }
+
+    // Find the topmost enclosing Join. For example, if the tree is
+    //   Join((Join(a, b), Join(c, Join(d, e)))
+    // we get the same topJoin for a, b, c, d and e. Join. Stack is never 
empty,
+    // and frame.r on the first iteration is always "join".
+    assert !stack.isEmpty();
+    assert stack.element().r == join;
+    Join j = null;
+    for (Frame frame : stack) {
+      j = (Join) frame.r;
+      if (!(frame.parent instanceof Join)) {
+        break;
+      }
+    }
+    final Join topJoin = requireNonNull(j, "top join");
+
+    // Flatten the join tree, using a breadth-first algorithm.
+    // After flattening, the list contains all of the joins that will make up
+    // the FROM clause.
+    final List<Join> flatJoins = new ArrayList<>();
+    flatJoins.add(topJoin);
+    for (int i = 0; i < flatJoins.size();) {
+      final Join j2 = flatJoins.get(i++);
+      if (j2.getLeft() instanceof Join) {
+        flatJoins.add((Join) j2.getLeft());
+      }
+      if (j2.getRight() instanceof Join) {
+        flatJoins.add((Join) j2.getRight());
+      }
+    }
+
+    // If all joins are cross-joins (INNER JOIN ON TRUE), we can use
+    // we can use comma syntax "FROM a, b, c, d, e".
+    for (Join j2 : flatJoins) {
+      if (j2.getJoinType() != JoinRelType.INNER
+          || !j2.getCondition().isAlwaysTrue()) {
+        return false;
+      }
+    }
+    return true;
   }
 
   /** Visits a Correlate; called by {@link #dispatch} via reflection. */
diff --git 
a/core/src/test/java/org/apache/calcite/rel/rel2sql/RelToSqlConverterTest.java 
b/core/src/test/java/org/apache/calcite/rel/rel2sql/RelToSqlConverterTest.java
index b41c9ad..51ed1f3 100644
--- 
a/core/src/test/java/org/apache/calcite/rel/rel2sql/RelToSqlConverterTest.java
+++ 
b/core/src/test/java/org/apache/calcite/rel/rel2sql/RelToSqlConverterTest.java
@@ -92,6 +92,7 @@ import java.util.Collection;
 import java.util.List;
 import java.util.Map;
 import java.util.Set;
+import java.util.function.Consumer;
 import java.util.function.Function;
 import java.util.function.UnaryOperator;
 import java.util.stream.Collectors;
@@ -5255,12 +5256,143 @@ class RelToSqlConverterTest {
     sql(query).ok(expected);
   }
 
-  @Test void testCrossJoinEmulationForSpark() {
-    String query = "select * from \"employee\", \"department\"";
-    final String expected = "SELECT *\n"
+  /** Test case for
+   * <a 
href="https://issues.apache.org/jira/browse/CALCITE-4485";>[CALCITE-4485]
+   * JDBC adapter generates invalid SQL when one of the joins is {@code INNER
+   * JOIN ... ON TRUE}</a>. */
+  @Test void testCommaCrossJoin() {
+    final Function<RelBuilder, RelNode> relFn = b ->
+        b.scan("tpch", "customer")
+            .aggregate(b.groupKey(b.field("nation_name")),
+                b.count().as("cnt1"))
+            .project(b.field("nation_name"), b.field("cnt1"))
+            .as("cust")
+            .scan("tpch", "lineitem")
+            .aggregate(b.groupKey(),
+                b.count().as("cnt2"))
+            .project(b.field("cnt2"))
+            .as("lineitem")
+            .join(JoinRelType.INNER)
+            .scan("tpch", "part")
+            .join(JoinRelType.LEFT,
+                b.equals(b.field(2, "cust", "nation_name"),
+                    b.field(2, "part", "p_brand")))
+            .project(b.field("cust", "nation_name"),
+                b.alias(
+                    b.call(SqlStdOperatorTable.MINUS,
+                        b.field("cnt1"),
+                        b.field("cnt2")),
+                    "f1"))
+            .build();
+
+    // For documentation purposes, here is the query that was generated before
+    // [CALCITE-4485] was fixed.
+    final String previousPostgresql = ""
+        + "SELECT \"t\".\"nation_name\", \"t\".\"cnt1\" - \"t0\".\"cnt2\" AS 
\"f1\"\n"
+        + "FROM (SELECT \"nation_name\", COUNT(*) AS \"cnt1\"\n"
+        + "FROM \"tpch\".\"customer\"\n"
+        + "GROUP BY \"nation_name\") AS \"t\",\n"
+        + "(SELECT COUNT(*) AS \"cnt2\"\n"
+        + "FROM \"tpch\".\"lineitem\") AS \"t0\"\n"
+        + "LEFT JOIN \"tpch\".\"part\" ON \"t\".\"nation_name\" = 
\"part\".\"p_brand\"";
+    final String expectedPostgresql = ""
+        + "SELECT \"t\".\"nation_name\", \"t\".\"cnt1\" - \"t0\".\"cnt2\" AS 
\"f1\"\n"
+        + "FROM (SELECT \"nation_name\", COUNT(*) AS \"cnt1\"\n"
+        + "FROM \"tpch\".\"customer\"\n"
+        + "GROUP BY \"nation_name\") AS \"t\"\n"
+        + "CROSS JOIN (SELECT COUNT(*) AS \"cnt2\"\n"
+        + "FROM \"tpch\".\"lineitem\") AS \"t0\"\n"
+        + "LEFT JOIN \"tpch\".\"part\" ON \"t\".\"nation_name\" = 
\"part\".\"p_brand\"";
+    relFn(relFn)
+        .schema(CalciteAssert.SchemaSpec.TPCH)
+        .withPostgresql()
+        .ok(expectedPostgresql);
+  }
+
+  /** A cartesian product is unparsed as a CROSS JOIN on Spark,
+   * comma join on other DBs.
+   *
+   * @see SqlDialect#emulateJoinTypeForCrossJoin()
+   */
+  @Test void testCrossJoinEmulation() {
+    final String expectedSpark = "SELECT *\n"
         + "FROM foodmart.employee\n"
         + "CROSS JOIN foodmart.department";
-    sql(query).withSpark().ok(expected);
+    final String expectedMysql = "SELECT *\n"
+        + "FROM `foodmart`.`employee`,\n"
+        + "`foodmart`.`department`";
+    Consumer<String> fn = sql ->
+        sql(sql)
+            .withSpark().ok(expectedSpark)
+            .withMysql().ok(expectedMysql);
+    fn.accept("select * from \"employee\", \"department\"");
+    fn.accept("select * from \"employee\" cross join \"department\"");
+    fn.accept("select * from \"employee\" join \"department\" on true");
+  }
+
+  /** Similar to {@link #testCommaCrossJoin()} (but uses SQL)
+   * and {@link #testCrossJoinEmulation()} (but is 3 way). We generate a comma
+   * join if the only joins are {@code CROSS JOIN} or
+   * {@code INNER JOIN ... ON TRUE}, and if we're not on Spark. */
+  @Test void testCommaCrossJoin3way() {
+    String sql = "select *\n"
+        + "from \"store\" as s\n"
+        + "inner join \"employee\" as e on true\n"
+        + "cross join \"department\" as d";
+    final String expectedMysql = "SELECT *\n"
+        + "FROM `foodmart`.`store`,\n"
+        + "`foodmart`.`employee`,\n"
+        + "`foodmart`.`department`";
+    final String expectedSpark = "SELECT *\n"
+        + "FROM foodmart.store\n"
+        + "CROSS JOIN foodmart.employee\n"
+        + "CROSS JOIN foodmart.department";
+    sql(sql)
+        .withMysql().ok(expectedMysql)
+        .withSpark().ok(expectedSpark);
+  }
+
+  /** As {@link #testCommaCrossJoin3way()}, but shows that if there is a
+   * {@code LEFT JOIN} in the FROM clause, we can't use comma-join. */
+  @Test void testLeftJoinPreventsCommaJoin() {
+    String sql = "select *\n"
+        + "from \"store\" as s\n"
+        + "left join \"employee\" as e on true\n"
+        + "cross join \"department\" as d";
+    final String expectedMysql = "SELECT *\n"
+        + "FROM `foodmart`.`store`\n"
+        + "LEFT JOIN `foodmart`.`employee` ON TRUE\n"
+        + "CROSS JOIN `foodmart`.`department`";
+    sql(sql).withMysql().ok(expectedMysql);
+  }
+
+  /** As {@link #testLeftJoinPreventsCommaJoin()}, but the non-cross-join
+   * occurs later in the FROM clause. */
+  @Test void testRightJoinPreventsCommaJoin() {
+    String sql = "select *\n"
+        + "from \"store\" as s\n"
+        + "cross join \"employee\" as e\n"
+        + "right join \"department\" as d on true";
+    final String expectedMysql = "SELECT *\n"
+        + "FROM `foodmart`.`store`\n"
+        + "CROSS JOIN `foodmart`.`employee`\n"
+        + "RIGHT JOIN `foodmart`.`department` ON TRUE";
+    sql(sql).withMysql().ok(expectedMysql);
+  }
+
+  /** As {@link #testLeftJoinPreventsCommaJoin()}, but the impediment is a
+   * {@code JOIN} whose condition is not {@code TRUE}. */
+  @Test void testOnConditionPreventsCommaJoin() {
+    String sql = "select *\n"
+        + "from \"store\" as s\n"
+        + "join \"employee\" as e on s.\"store_id\" = e.\"store_id\"\n"
+        + "cross join \"department\" as d";
+    final String expectedMysql = "SELECT *\n"
+        + "FROM `foodmart`.`store`\n"
+        + "INNER JOIN `foodmart`.`employee`"
+        + " ON `store`.`store_id` = `employee`.`store_id`\n"
+        + "CROSS JOIN `foodmart`.`department`";
+    sql(sql).withMysql().ok(expectedMysql);
   }
 
   @Test void testSubstringInSpark() {

Reply via email to