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 c3108bc1231b2aa4ba25227d225544fef4576508
Author: Julian Hyde <[email protected]>
AuthorDate: Fri Jul 12 14:33:12 2019 -0700

    [CALCITE-3191] In JDBC adapter for MySQL, implement Values by generating 
SELECT without FROM
    
    Revisit SqlDialect methods supportsAliasedValues() and 
hasImplicitTableAlias();
    make requiresAliasForFromItems() public (was protected),
    and add List<String> getSingleRowTableName().
---
 .../calcite/rel/rel2sql/RelToSqlConverter.java     | 56 +++++++++-------
 .../apache/calcite/rel/rel2sql/SqlImplementor.java | 38 ++++++++++-
 .../java/org/apache/calcite/sql/SqlDialect.java    | 29 ++++++++-
 .../calcite/sql/dialect/MysqlSqlDialect.java       |  9 +++
 .../calcite/sql/dialect/OracleSqlDialect.java      | 14 +++-
 .../calcite/sql/dialect/PostgresqlSqlDialect.java  |  2 +-
 .../calcite/rel/rel2sql/RelToSqlConverterTest.java | 74 +++++++++++++++++++---
 7 files changed, 184 insertions(+), 38 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 c6a707d..9981ce7 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
@@ -381,14 +381,12 @@ public class RelToSqlConverter extends SqlImplementor
         final List<SqlNode> values2 = new ArrayList<>();
         final SqlNodeList exprList = exprList(context, tuple);
         for (Pair<SqlNode, String> value : Pair.zip(exprList, fieldNames)) {
-          values2.add(
-              SqlStdOperatorTable.AS.createCall(POS, value.left,
-                  new SqlIdentifier(value.right, POS)));
+          values2.add(as(value.left, value.right));
         }
         list.add(
             new SqlSelect(POS, null,
                 new SqlNodeList(values2, POS),
-                new SqlIdentifier("DUAL", POS), null, null,
+                getDual(), null, null,
                 null, null, null, null, null));
       }
       if (list.isEmpty()) {
@@ -397,15 +395,27 @@ public class RelToSqlConverter extends SqlImplementor
         // This would return an empty result set with the same number of 
columns as the field names.
         final List<SqlNode> nullColumnNames = new ArrayList<>();
         for (String fieldName : fieldNames) {
-          SqlCall nullColumnName = SqlStdOperatorTable.AS.createCall(
-              POS, SqlLiteral.createNull(POS),
-              new SqlIdentifier(fieldName, POS));
+          SqlCall nullColumnName = as(SqlLiteral.createNull(POS), fieldName);
           nullColumnNames.add(nullColumnName);
         }
-        query = new SqlSelect(POS, null,
-            new SqlNodeList(nullColumnNames, POS),
-            new SqlIdentifier("DUAL", POS), createAlwaysFalseCondition(), null,
-            null, null, null, null, null);
+        final SqlIdentifier dual = getDual();
+        if (dual == null) {
+          query = new SqlSelect(POS, null,
+              new SqlNodeList(nullColumnNames, POS), null, null, null, null,
+              null, null, null, null);
+
+          // Wrap "SELECT 1 AS x"
+          // as "SELECT * FROM (SELECT 1 AS x) AS t WHERE false"
+          query = new SqlSelect(POS, null,
+              new SqlNodeList(ImmutableList.of(SqlIdentifier.star(POS)), POS),
+              as(query, "t"), createAlwaysFalseCondition(), null, null,
+              null, null, null, null);
+        } else {
+          query = new SqlSelect(POS, null,
+              new SqlNodeList(nullColumnNames, POS),
+              dual, createAlwaysFalseCondition(), null,
+              null, null, null, null, null);
+        }
       } else if (list.size() == 1) {
         query = list.get(0);
       } else {
@@ -434,15 +444,12 @@ public class RelToSqlConverter extends SqlImplementor
       }
       query = SqlStdOperatorTable.VALUES.createCall(selects);
       if (rename) {
-        final List<SqlNode> list = new ArrayList<>();
-        list.add(query);
-        list.add(new SqlIdentifier("t", POS));
-        for (String fieldName : fieldNames) {
-          list.add(new SqlIdentifier(fieldName, POS));
-        }
-        query = SqlStdOperatorTable.AS.createCall(POS, list);
+        query = as(query, "t", fieldNames.toArray(new String[0]));
       }
       if (isEmpty) {
+        if (!rename) {
+          query = as(query, "t");
+        }
         query = new SqlSelect(POS, null,
                 null, query,
                 createAlwaysFalseCondition(),
@@ -453,6 +460,14 @@ public class RelToSqlConverter extends SqlImplementor
     return result(query, clauses, e, null);
   }
 
+  private SqlIdentifier getDual() {
+    final List<String> names = dialect.getSingleRowTableName();
+    if (names == null) {
+      return null;
+    }
+    return new SqlIdentifier(names, POS);
+  }
+
   private SqlNode createAlwaysFalseCondition() {
     // Building the select query in the form:
     // select * from VALUES(NULL,NULL ...) where 1=0
@@ -698,11 +713,6 @@ public class RelToSqlConverter extends SqlImplementor
     return result(matchRecognize, Expressions.list(Clause.FROM), e, null);
   }
 
-  private SqlCall as(SqlNode e, String alias) {
-    return SqlStdOperatorTable.AS.createCall(POS, e,
-        new SqlIdentifier(alias, POS));
-  }
-
   @Override public void addSelect(List<SqlNode> selectList, SqlNode node,
       RelDataType rowType) {
     String name = rowType.getFieldNames().get(selectList.size());
diff --git 
a/core/src/main/java/org/apache/calcite/rel/rel2sql/SqlImplementor.java 
b/core/src/main/java/org/apache/calcite/rel/rel2sql/SqlImplementor.java
index 3371209..cca13f7 100644
--- a/core/src/main/java/org/apache/calcite/rel/rel2sql/SqlImplementor.java
+++ b/core/src/main/java/org/apache/calcite/rel/rel2sql/SqlImplementor.java
@@ -123,12 +123,25 @@ public abstract class SqlImplementor {
     String name = rowType.getFieldNames().get(selectList.size());
     String alias = SqlValidatorUtil.getAlias(node, -1);
     if (alias == null || !alias.equals(name)) {
-      node = SqlStdOperatorTable.AS.createCall(
-          POS, node, new SqlIdentifier(name, POS));
+      node = as(node, name);
     }
     selectList.add(node);
   }
 
+  /** Convenience method for creating column and table aliases.
+   *
+   * <p>{@code AS(e, "c")} creates "e AS c";
+   * {@code AS(e, "t", "c1", "c2"} creates "e AS t (c1, c2)". */
+  protected SqlCall as(SqlNode e, String alias, String... fieldNames) {
+    final List<SqlNode> operandList = new ArrayList<>();
+    operandList.add(e);
+    operandList.add(new SqlIdentifier(alias, POS));
+    for (String fieldName : fieldNames) {
+      operandList.add(new SqlIdentifier(fieldName, POS));
+    }
+    return SqlStdOperatorTable.AS.createCall(POS, operandList);
+  }
+
   /** Returns whether a list of expressions projects all fields, in order,
    * from the input, with the same names. */
   public static boolean isStar(List<RexNode> exps, RelDataType inputRowType,
@@ -413,10 +426,31 @@ public abstract class SqlImplementor {
                 || ((SqlCall) node).getOperator() == SqlStdOperatorTable.AS
                 || ((SqlCall) node).getOperator() == 
SqlStdOperatorTable.VALUES)
         : node;
+    if (requiresAlias(node)) {
+      node = as(node, "t");
+    }
     return new SqlSelect(POS, SqlNodeList.EMPTY, null, node, null, null, null,
         SqlNodeList.EMPTY, null, null, null);
   }
 
+  /** Returns whether we need to add an alias if this node is to be the FROM
+   * clause of a SELECT. */
+  private boolean requiresAlias(SqlNode node) {
+    if (!dialect.requiresAliasForFromItems()) {
+      return false;
+    }
+    switch (node.getKind()) {
+    case IDENTIFIER:
+      return !dialect.hasImplicitTableAlias();
+    case AS:
+    case JOIN:
+    case EXPLICIT_TABLE:
+      return false;
+    default:
+      return true;
+    }
+  }
+
   /** Context for translating a {@link RexNode} expression (within a
    * {@link RelNode}) into a {@link SqlNode} expression (within a SQL parse
    * tree). */
diff --git a/core/src/main/java/org/apache/calcite/sql/SqlDialect.java 
b/core/src/main/java/org/apache/calcite/sql/SqlDialect.java
index 237f8b3..d362ff2 100644
--- a/core/src/main/java/org/apache/calcite/sql/SqlDialect.java
+++ b/core/src/main/java/org/apache/calcite/sql/SqlDialect.java
@@ -54,6 +54,7 @@ import java.util.Objects;
 import java.util.Set;
 import java.util.function.Supplier;
 import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
 
 /**
  * <code>SqlDialect</code> encapsulates the differences between dialects of 
SQL.
@@ -588,7 +589,20 @@ public class SqlDialect {
   }
 
   // -- behaviors --
-  protected boolean requiresAliasForFromItems() {
+
+  /** Whether a sub-query in the FROM clause must have an alias.
+   *
+   * <p>For example, in PostgreSQL, this query is legal:
+   *
+   * <blockquote>{@code SELECT * FROM (SELECT * FROM Emp) As e}</blockquote>
+   *
+   * <p>but remove the alias {@code e} and it is not:
+   *
+   * <blockquote>{@code SELECT * FROM (SELECT * FROM Emp)}</blockquote>
+   *
+   * <p>In Oracle, both queries are legal.
+   */
+  public boolean requiresAliasForFromItems() {
     return false;
   }
 
@@ -979,6 +993,19 @@ public class SqlDialect {
     return true;
   }
 
+  /** Returns the name of the system table that has precisely one row.
+   * If there is no such table, returns null, and we will generate SELECT with
+   * no FROM clause.
+   *
+   * <p>For {@code VALUES 1},
+   * Oracle returns ["DUAL"] and we generate "SELECT 1 FROM DUAL";
+   * MySQL returns null and we generate "SELECT 1".
+   */
+  @Experimental
+  public @Nullable List<String> getSingleRowTableName() {
+    return null;
+  }
+
   /**
    * Copies settings from this dialect into a parser configuration.
    *
diff --git 
a/core/src/main/java/org/apache/calcite/sql/dialect/MysqlSqlDialect.java 
b/core/src/main/java/org/apache/calcite/sql/dialect/MysqlSqlDialect.java
index 742bd02..2c55466 100644
--- a/core/src/main/java/org/apache/calcite/sql/dialect/MysqlSqlDialect.java
+++ b/core/src/main/java/org/apache/calcite/sql/dialect/MysqlSqlDialect.java
@@ -72,6 +72,15 @@ public class MysqlSqlDialect extends SqlDialect {
     return false;
   }
 
+  @Override public boolean requiresAliasForFromItems() {
+    return true;
+  }
+
+  public boolean supportsAliasedValues() {
+    // MySQL supports VALUES only in INSERT; not in a FROM clause
+    return false;
+  }
+
   @Override public void unparseOffsetFetch(SqlWriter writer, SqlNode offset,
       SqlNode fetch) {
     unparseFetchUsingLimit(writer, offset, fetch);
diff --git 
a/core/src/main/java/org/apache/calcite/sql/dialect/OracleSqlDialect.java 
b/core/src/main/java/org/apache/calcite/sql/dialect/OracleSqlDialect.java
index 14c72da..4217005 100644
--- a/core/src/main/java/org/apache/calcite/sql/dialect/OracleSqlDialect.java
+++ b/core/src/main/java/org/apache/calcite/sql/dialect/OracleSqlDialect.java
@@ -38,6 +38,10 @@ import org.apache.calcite.sql.fun.SqlStdOperatorTable;
 import org.apache.calcite.sql.parser.SqlParserPos;
 import org.apache.calcite.sql.type.SqlTypeName;
 
+import com.google.common.collect.ImmutableList;
+
+import java.util.List;
+
 /**
  * A <code>SqlDialect</code> implementation for the Oracle database.
  */
@@ -115,19 +119,23 @@ public class OracleSqlDialect extends SqlDialect {
   @Override public void unparseDateTimeLiteral(SqlWriter writer,
       SqlAbstractDateTimeLiteral literal, int leftPrec, int rightPrec) {
     if (literal instanceof SqlTimestampLiteral) {
-      writer.literal("TO_TIMESTAMP ('"
+      writer.literal("TO_TIMESTAMP('"
           + literal.toFormattedString() + "', 'YYYY-MM-DD HH24:MI:SS.FF')");
     } else if (literal instanceof SqlDateLiteral) {
-      writer.literal("TO_DATE ('"
+      writer.literal("TO_DATE('"
           + literal.toFormattedString() + "', 'YYYY-MM-DD')");
     } else if (literal instanceof SqlTimeLiteral) {
-      writer.literal("TO_TIME ('"
+      writer.literal("TO_TIME('"
           + literal.toFormattedString() + "', 'HH24:MI:SS.FF')");
     } else {
       super.unparseDateTimeLiteral(writer, literal, leftPrec, rightPrec);
     }
   }
 
+  @Override public List<String> getSingleRowTableName() {
+    return ImmutableList.of("DUAL");
+  }
+
   @Override public void unparseCall(SqlWriter writer, SqlCall call,
       int leftPrec, int rightPrec) {
     if (call.getOperator() == SqlStdOperatorTable.SUBSTRING) {
diff --git 
a/core/src/main/java/org/apache/calcite/sql/dialect/PostgresqlSqlDialect.java 
b/core/src/main/java/org/apache/calcite/sql/dialect/PostgresqlSqlDialect.java
index 987c944..8a4cc77 100644
--- 
a/core/src/main/java/org/apache/calcite/sql/dialect/PostgresqlSqlDialect.java
+++ 
b/core/src/main/java/org/apache/calcite/sql/dialect/PostgresqlSqlDialect.java
@@ -91,7 +91,7 @@ public class PostgresqlSqlDialect extends SqlDialect {
         -1, -1, null, null, SqlParserPos.ZERO);
   }
 
-  @Override protected boolean requiresAliasForFromItems() {
+  @Override public boolean requiresAliasForFromItems() {
     return true;
   }
 
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 eea7962..f013e1f 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
@@ -18,12 +18,13 @@ package org.apache.calcite.rel.rel2sql;
 
 import org.apache.calcite.config.NullCollation;
 import org.apache.calcite.plan.RelOptPlanner;
+import org.apache.calcite.plan.RelOptRule;
 import org.apache.calcite.plan.RelTraitDef;
 import org.apache.calcite.plan.hep.HepPlanner;
-import org.apache.calcite.plan.hep.HepProgram;
 import org.apache.calcite.plan.hep.HepProgramBuilder;
 import org.apache.calcite.rel.RelNode;
 import org.apache.calcite.rel.core.JoinRelType;
+import org.apache.calcite.rel.rules.PruneEmptyRules;
 import org.apache.calcite.rel.rules.UnionMergeRule;
 import org.apache.calcite.rel.type.RelDataType;
 import org.apache.calcite.rel.type.RelDataTypeFactory;
@@ -61,6 +62,7 @@ import org.apache.calcite.tools.RelBuilder;
 import org.apache.calcite.tools.RuleSet;
 import org.apache.calcite.tools.RuleSets;
 import org.apache.calcite.util.TestUtil;
+import org.apache.calcite.util.Util;
 
 import com.google.common.collect.ImmutableList;
 import com.google.common.collect.ImmutableMap;
@@ -735,6 +737,27 @@ public class RelToSqlConverterTest {
     sql(query).ok(expected);
   }
 
+  @Test public void testHaving4() {
+    final String query = "select \"product_id\"\n"
+        + "from (\n"
+        + "  select \"product_id\", avg(\"gross_weight\") as agw\n"
+        + "  from \"product\"\n"
+        + "  where \"net_weight\" < 100\n"
+        + "  group by \"product_id\")\n"
+        + "where agw > 50\n"
+        + "group by \"product_id\"\n"
+        + "having avg(agw) > 60\n";
+    final String expected = "SELECT \"product_id\"\n"
+        + "FROM (SELECT \"product_id\", AVG(\"gross_weight\") AS \"AGW\"\n"
+        + "FROM \"foodmart\".\"product\"\n"
+        + "WHERE \"net_weight\" < 100\n"
+        + "GROUP BY \"product_id\"\n"
+        + "HAVING AVG(\"gross_weight\") > 50) AS \"t2\"\n"
+        + "GROUP BY \"product_id\"\n"
+        + "HAVING AVG(\"AGW\") > 60";
+    sql(query).ok(expected);
+  }
+
   @Test public void testSelectQueryWithOrderByClause() {
     String query = "select \"product_id\"  from \"product\" order by 
\"net_weight\"";
     final String expected = "SELECT \"product_id\", \"net_weight\"\n"
@@ -1662,11 +1685,9 @@ public class RelToSqlConverterTest {
         + "SELECT \"product_class_id\" AS \"PRODUCT_ID\"\n"
         + "FROM \"foodmart\".\"product_class\"";
 
-    final HepProgram program =
-        new HepProgramBuilder().addRuleClass(UnionMergeRule.class).build();
     final RuleSet rules = RuleSets.ofList(UnionMergeRule.INSTANCE);
     sql(query)
-        .optimize(rules, new HepPlanner(program))
+        .optimize(rules, null)
         .ok(expected);
   }
 
@@ -3180,6 +3201,10 @@ public class RelToSqlConverterTest {
     final String expectedHsqldb = "SELECT a\n"
         + "FROM (VALUES  (1, 'x '),\n"
         + " (2, 'yy')) AS t (a, b)";
+    final String expectedMysql = "SELECT `a`\n"
+        + "FROM (SELECT 1 AS `a`, 'x ' AS `b`\n"
+        + "UNION ALL\n"
+        + "SELECT 2 AS `a`, 'yy' AS `b`) AS `t`";
     final String expectedPostgresql = "SELECT \"a\"\n"
         + "FROM (VALUES  (1, 'x '),\n"
         + " (2, 'yy')) AS \"t\" (\"a\", \"b\")";
@@ -3194,6 +3219,8 @@ public class RelToSqlConverterTest {
     sql(sql)
         .withHsqldb()
         .ok(expectedHsqldb)
+        .withMysql()
+        .ok(expectedMysql)
         .withPostgresql()
         .ok(expectedPostgresql)
         .withOracle()
@@ -3204,6 +3231,31 @@ public class RelToSqlConverterTest {
         .ok(expectedRedshift);
   }
 
+  @Test public void testValuesEmpty() {
+    final String sql = "select *\n"
+        + "from (values (1, 'a'), (2, 'bb')) as t(x, y)\n"
+        + "limit 0";
+    final RuleSet rules =
+        RuleSets.ofList(PruneEmptyRules.SORT_FETCH_ZERO_INSTANCE);
+    final String expectedMysql = "SELECT *\n"
+        + "FROM (SELECT NULL AS `X`, NULL AS `Y`) AS `t`\n"
+        + "WHERE 1 = 0";
+    final String expectedOracle = "SELECT NULL \"X\", NULL \"Y\"\n"
+        + "FROM \"DUAL\"\n"
+        + "WHERE 1 = 0";
+    final String expectedPostgresql = "SELECT *\n"
+        + "FROM (VALUES  (NULL, NULL)) AS \"t\" (\"X\", \"Y\")\n"
+        + "WHERE 1 = 0";
+    sql(sql)
+        .optimize(rules, null)
+        .withMysql()
+        .ok(expectedMysql)
+        .withOracle()
+        .ok(expectedOracle)
+        .withPostgresql()
+        .ok(expectedPostgresql);
+  }
+
   /** Test case for
    * <a 
href="https://issues.apache.org/jira/browse/CALCITE-2118";>[CALCITE-2118]
    * RelToSqlConverter should only generate "*" if field names match</a>. */
@@ -3603,7 +3655,7 @@ public class RelToSqlConverterTest {
 
   @Test public void testDateLiteralOracle() {
     String query = "SELECT DATE '1978-05-02' FROM \"employee\"";
-    String expected = "SELECT TO_DATE ('1978-05-02', 'YYYY-MM-DD')\n"
+    String expected = "SELECT TO_DATE('1978-05-02', 'YYYY-MM-DD')\n"
         + "FROM \"foodmart\".\"employee\"";
     sql(query)
         .withOracle()
@@ -3612,7 +3664,8 @@ public class RelToSqlConverterTest {
 
   @Test public void testTimestampLiteralOracle() {
     String query = "SELECT TIMESTAMP '1978-05-02 12:34:56.78' FROM 
\"employee\"";
-    String expected = "SELECT TO_TIMESTAMP ('1978-05-02 12:34:56.78', 
'YYYY-MM-DD HH24:MI:SS.FF')\n"
+    String expected = "SELECT TO_TIMESTAMP('1978-05-02 12:34:56.78',"
+        + " 'YYYY-MM-DD HH24:MI:SS.FF')\n"
         + "FROM \"foodmart\".\"employee\"";
     sql(query)
         .withOracle()
@@ -3621,7 +3674,7 @@ public class RelToSqlConverterTest {
 
   @Test public void testTimeLiteralOracle() {
     String query = "SELECT TIME '12:34:56.78' FROM \"employee\"";
-    String expected = "SELECT TO_TIME ('12:34:56.78', 'HH24:MI:SS.FF')\n"
+    String expected = "SELECT TO_TIME('12:34:56.78', 'HH24:MI:SS.FF')\n"
         + "FROM \"foodmart\".\"employee\"";
     sql(query)
         .withOracle()
@@ -3798,7 +3851,12 @@ public class RelToSqlConverterTest {
       return new Sql(schema, sql, dialect, config,
           FlatLists.append(transforms, r -> {
             Program program = Programs.of(ruleSet);
-            return program.run(relOptPlanner, r, r.getTraitSet(),
+            final RelOptPlanner p =
+                Util.first(relOptPlanner,
+                    new HepPlanner(
+                        new HepProgramBuilder().addRuleClass(RelOptRule.class)
+                            .build()));
+            return program.run(p, r, r.getTraitSet(),
                 ImmutableList.of(), ImmutableList.of());
           }));
     }

Reply via email to