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

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


The following commit(s) were added to refs/heads/master by this push:
     new 39e5856  [CALCITE-2157] ClickHouse dialect implementation (Chris 
Baynes)
39e5856 is described below

commit 39e58566c1ac02824d99ae9260d3315539efd57e
Author: Chris Baynes <binaryexp+git...@gmail.com>
AuthorDate: Tue Jan 30 14:13:12 2018 +0100

    [CALCITE-2157] ClickHouse dialect implementation (Chris Baynes)
    
    close apache/calcite#618
---
 .../java/org/apache/calcite/sql/SqlDialect.java    |   4 +
 .../apache/calcite/sql/SqlDialectFactoryImpl.java  |   5 +
 .../calcite/sql/dialect/ClickHouseSqlDialect.java  | 242 +++++++++++++++++++++
 .../calcite/rel/rel2sql/RelToSqlConverterTest.java |  83 ++++++-
 4 files changed, 333 insertions(+), 1 deletion(-)

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 07f82c6..2ef11f9 100644
--- a/core/src/main/java/org/apache/calcite/sql/SqlDialect.java
+++ b/core/src/main/java/org/apache/calcite/sql/SqlDialect.java
@@ -274,6 +274,9 @@ public class SqlDialect {
     case "ACCESS":
       return DatabaseProduct.ACCESS;
     case "APACHE DERBY":
+      return DatabaseProduct.DERBY;
+    case "CLICKHOUSE":
+      return DatabaseProduct.CLICKHOUSE;
     case "DBMS:CLOUDSCAPE":
       return DatabaseProduct.DERBY;
     case "HIVE":
@@ -1226,6 +1229,7 @@ public class SqlDialect {
     ACCESS("Access", "\"", NullCollation.HIGH),
     BIG_QUERY("Google BigQuery", "`", NullCollation.LOW),
     CALCITE("Apache Calcite", "\"", NullCollation.HIGH),
+    CLICKHOUSE("ClickHouse", "`", NullCollation.LOW),
     MSSQL("Microsoft SQL Server", "[", NullCollation.HIGH),
     MYSQL("MySQL", "`", NullCollation.LOW),
     ORACLE("Oracle", "\"", NullCollation.HIGH),
diff --git 
a/core/src/main/java/org/apache/calcite/sql/SqlDialectFactoryImpl.java 
b/core/src/main/java/org/apache/calcite/sql/SqlDialectFactoryImpl.java
index 6d265f1..82d07cf 100644
--- a/core/src/main/java/org/apache/calcite/sql/SqlDialectFactoryImpl.java
+++ b/core/src/main/java/org/apache/calcite/sql/SqlDialectFactoryImpl.java
@@ -22,6 +22,7 @@ import org.apache.calcite.sql.dialect.AccessSqlDialect;
 import org.apache.calcite.sql.dialect.AnsiSqlDialect;
 import org.apache.calcite.sql.dialect.BigQuerySqlDialect;
 import org.apache.calcite.sql.dialect.CalciteSqlDialect;
+import org.apache.calcite.sql.dialect.ClickHouseSqlDialect;
 import org.apache.calcite.sql.dialect.Db2SqlDialect;
 import org.apache.calcite.sql.dialect.DerbySqlDialect;
 import org.apache.calcite.sql.dialect.FirebirdSqlDialect;
@@ -102,6 +103,8 @@ public class SqlDialectFactoryImpl implements 
SqlDialectFactory {
       return new AccessSqlDialect(c);
     case "APACHE DERBY":
       return new DerbySqlDialect(c);
+    case "CLICKHOUSE":
+      return new ClickHouseSqlDialect(c);
     case "DBMS:CLOUDSCAPE":
       return new DerbySqlDialect(c);
     case "HIVE":
@@ -244,6 +247,8 @@ public class SqlDialectFactoryImpl implements 
SqlDialectFactory {
       return BigQuerySqlDialect.DEFAULT;
     case CALCITE:
       return CalciteSqlDialect.DEFAULT;
+    case CLICKHOUSE:
+      return ClickHouseSqlDialect.DEFAULT;
     case DB2:
       return Db2SqlDialect.DEFAULT;
     case DERBY:
diff --git 
a/core/src/main/java/org/apache/calcite/sql/dialect/ClickHouseSqlDialect.java 
b/core/src/main/java/org/apache/calcite/sql/dialect/ClickHouseSqlDialect.java
new file mode 100644
index 0000000..a05799d
--- /dev/null
+++ 
b/core/src/main/java/org/apache/calcite/sql/dialect/ClickHouseSqlDialect.java
@@ -0,0 +1,242 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to you under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.calcite.sql.dialect;
+
+import org.apache.calcite.avatica.util.TimeUnitRange;
+import org.apache.calcite.config.NullCollation;
+import org.apache.calcite.rel.type.RelDataType;
+import org.apache.calcite.sql.SqlAbstractDateTimeLiteral;
+import org.apache.calcite.sql.SqlBasicTypeNameSpec;
+import org.apache.calcite.sql.SqlCall;
+import org.apache.calcite.sql.SqlDataTypeSpec;
+import org.apache.calcite.sql.SqlDateLiteral;
+import org.apache.calcite.sql.SqlDialect;
+import org.apache.calcite.sql.SqlKind;
+import org.apache.calcite.sql.SqlLiteral;
+import org.apache.calcite.sql.SqlNode;
+import org.apache.calcite.sql.SqlSpecialOperator;
+import org.apache.calcite.sql.SqlTimeLiteral;
+import org.apache.calcite.sql.SqlTimestampLiteral;
+import org.apache.calcite.sql.SqlWriter;
+import org.apache.calcite.sql.fun.SqlStdOperatorTable;
+import org.apache.calcite.sql.parser.SqlParserPos;
+import org.apache.calcite.sql.type.BasicSqlType;
+import org.apache.calcite.sql.type.SqlTypeName;
+
+import com.google.common.base.Preconditions;
+
+/**
+ * A <code>SqlDialect</code> implementation for the ClickHouse database.
+ */
+public class ClickHouseSqlDialect extends SqlDialect {
+  public static final SqlDialect.Context DEFAULT_CONTEXT = 
SqlDialect.EMPTY_CONTEXT
+      .withDatabaseProduct(SqlDialect.DatabaseProduct.CLICKHOUSE)
+      .withIdentifierQuoteString("`")
+      .withNullCollation(NullCollation.LOW);
+
+  public static final SqlDialect DEFAULT = new 
ClickHouseSqlDialect(DEFAULT_CONTEXT);
+
+  private static final SqlSpecialOperator CLICKHOUSE_SUBSTRING =
+      new SqlSpecialOperator("substring", SqlKind.OTHER_FUNCTION) {
+        public void unparse(
+            SqlWriter writer,
+            SqlCall call,
+            int leftPrec,
+            int rightPrec) {
+          writer.print(getName());
+          final SqlWriter.Frame frame =
+              writer.startList(SqlWriter.FrameTypeEnum.FUN_CALL, "(", ")");
+          for (SqlNode operand : call.getOperandList()) {
+            writer.sep(",");
+            operand.unparse(writer, 0, 0);
+          }
+          writer.endList(frame);
+        }
+      };
+
+  /** Creates a ClickHouseSqlDialect. */
+  public ClickHouseSqlDialect(Context context) {
+    super(context);
+  }
+
+  @Override public boolean supportsCharSet() {
+    return false;
+  }
+
+  @Override public boolean supportsNestedAggregations() {
+    return false;
+  }
+
+  @Override public boolean supportsWindowFunctions() {
+    return false;
+  }
+
+  @Override public CalendarPolicy getCalendarPolicy() {
+    return CalendarPolicy.SHIFT;
+  }
+
+  @Override public SqlNode getCastSpec(RelDataType type) {
+    if (type instanceof BasicSqlType) {
+      SqlTypeName typeName = type.getSqlTypeName();
+      switch (typeName) {
+      case VARCHAR:
+        return createSqlDataTypeSpecByName("String", typeName);
+      case TINYINT:
+        return createSqlDataTypeSpecByName("Int8", typeName);
+      case SMALLINT:
+        return createSqlDataTypeSpecByName("Int16", typeName);
+      case INTEGER:
+        return createSqlDataTypeSpecByName("Int32", typeName);
+      case BIGINT:
+        return createSqlDataTypeSpecByName("Int64", typeName);
+      case FLOAT:
+        return createSqlDataTypeSpecByName("Float32", typeName);
+      case DOUBLE:
+        return createSqlDataTypeSpecByName("Float64", typeName);
+      case DATE:
+        return createSqlDataTypeSpecByName("Date", typeName);
+      case TIMESTAMP:
+      case TIMESTAMP_WITH_LOCAL_TIME_ZONE:
+        return createSqlDataTypeSpecByName("DateTime", typeName);
+      }
+    }
+
+    return super.getCastSpec(type);
+  }
+
+  private SqlDataTypeSpec createSqlDataTypeSpecByName(String typeAlias, 
SqlTypeName typeName) {
+    SqlBasicTypeNameSpec spec = new SqlBasicTypeNameSpec(typeName, 
SqlParserPos.ZERO) {
+      @Override public void unparse(SqlWriter writer, int leftPrec, int 
rightPrec) {
+        // unparse as an identifier to ensure that type names are cased 
correctly
+        writer.identifier(typeAlias, true);
+      }
+    };
+    return new SqlDataTypeSpec(spec, SqlParserPos.ZERO);
+  }
+
+  @Override public void unparseDateTimeLiteral(SqlWriter writer,
+      SqlAbstractDateTimeLiteral literal, int leftPrec, int rightPrec) {
+    String toFunc;
+    if (literal instanceof SqlDateLiteral) {
+      toFunc = "toDate";
+    } else if (literal instanceof SqlTimestampLiteral) {
+      toFunc = "toDateTime";
+    } else if (literal instanceof SqlTimeLiteral) {
+      toFunc = "toTime";
+    } else {
+      throw new RuntimeException("ClickHouse does not support DateTime 
literal: "
+          + literal);
+    }
+
+    writer.literal(toFunc + "('" + literal.toFormattedString() + "')");
+  }
+
+  @Override public void unparseOffsetFetch(SqlWriter writer, SqlNode offset,
+      SqlNode fetch) {
+    Preconditions.checkArgument(fetch != null);
+
+    writer.newlineAndIndent();
+    final SqlWriter.Frame frame =
+        writer.startList(SqlWriter.FrameTypeEnum.FETCH);
+    writer.keyword("LIMIT");
+
+    if (offset != null) {
+      offset.unparse(writer, -1, -1);
+      writer.sep(",", true);
+    }
+
+    fetch.unparse(writer, -1, -1);
+    writer.endList(frame);
+  }
+
+  @Override public void unparseCall(SqlWriter writer, SqlCall call,
+      int leftPrec, int rightPrec) {
+    if (call.getOperator() == SqlStdOperatorTable.SUBSTRING) {
+      CLICKHOUSE_SUBSTRING.unparse(writer, call, 0, 0);
+    } else {
+      switch (call.getKind()) {
+      case FLOOR:
+        if (call.operandCount() != 2) {
+          super.unparseCall(writer, call, leftPrec, rightPrec);
+          return;
+        }
+
+        unparseFloor(writer, call);
+        break;
+
+      case COUNT:
+        // CH returns NULL rather than 0 for COUNT(DISTINCT) of NULL values.
+        // https://github.com/yandex/ClickHouse/issues/2494
+        // Wrap the call in a CH specific coalesce (assumeNotNull).
+        if (call.getFunctionQuantifier() != null
+            && call.getFunctionQuantifier().toString().equals("DISTINCT")) {
+          writer.print("assumeNotNull");
+          SqlWriter.Frame frame = writer.startList("(", ")");
+          super.unparseCall(writer, call, leftPrec, rightPrec);
+          writer.endList(frame);
+        } else {
+          super.unparseCall(writer, call, leftPrec, rightPrec);
+        }
+        break;
+
+      default:
+        super.unparseCall(writer, call, leftPrec, rightPrec);
+      }
+    }
+  }
+
+  /**
+   * Unparses datetime floor for ClickHouse.
+   *
+   * @param writer Writer
+   * @param call Call
+   */
+  private void unparseFloor(SqlWriter writer, SqlCall call) {
+    final SqlLiteral timeUnitNode = call.operand(1);
+    TimeUnitRange unit = (TimeUnitRange) timeUnitNode.getValue();
+
+    String funName;
+    switch (unit) {
+    case YEAR:
+      funName = "toStartOfYear";
+      break;
+    case MONTH:
+      funName = "toStartOfMonth";
+      break;
+    case WEEK:
+      funName = "toMonday";
+      break;
+    case DAY:
+      funName = "toDate";
+      break;
+    case HOUR:
+      funName = "toStartOfHour";
+      break;
+    case MINUTE:
+      funName = "toStartOfMinute";
+      break;
+    default:
+      throw new RuntimeException("ClickHouse does not support FLOOR for time 
unit: "
+          + unit);
+    }
+
+    writer.print(funName);
+    SqlWriter.Frame frame = writer.startList("(", ")");
+    call.operand(0).unparse(writer, 0, 0);
+    writer.endList(frame);
+  }
+}
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 3ecb46f..9e53070 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
@@ -2060,13 +2060,32 @@ class RelToSqlConverterTest {
     sql(query).dialect(mySqlDialect(NullCollation.LAST)).ok(expected);
   }
 
+  @Test void testCastToVarchar() {
+    String query = "select cast(\"product_id\" as varchar) from \"product\"";
+    final String expectedClickHouse = "SELECT CAST(`product_id` AS `String`)\n"
+        + "FROM `foodmart`.`product`";
+    final String expectedMysql = "SELECT CAST(`product_id` AS CHAR)\n"
+        + "FROM `foodmart`.`product`";
+    sql(query)
+        .withClickHouse()
+        .ok(expectedClickHouse)
+        .withMysql()
+        .ok(expectedMysql);
+  }
+
   @Test void testSelectQueryWithLimitClauseWithoutOrder() {
     String query = "select \"product_id\" from \"product\" limit 100 offset 
10";
     final String expected = "SELECT \"product_id\"\n"
         + "FROM \"foodmart\".\"product\"\n"
         + "OFFSET 10 ROWS\n"
         + "FETCH NEXT 100 ROWS ONLY";
-    sql(query).ok(expected);
+    final String expectedClickHouse = "SELECT `product_id`\n"
+        + "FROM `foodmart`.`product`\n"
+        + "LIMIT 10, 100";
+    sql(query)
+        .ok(expected)
+        .withClickHouse()
+        .ok(expectedClickHouse);
   }
 
   @Test void testSelectQueryWithLimitOffsetClause() {
@@ -2790,6 +2809,14 @@ class RelToSqlConverterTest {
         .ok(expected);
   }
 
+  @Test void testFloorClickHouse() {
+    String query = "SELECT floor(\"hire_date\" TO MINUTE) FROM \"employee\"";
+    String expected = "SELECT toStartOfMinute(`hire_date`)\nFROM 
`foodmart`.`employee`";
+    sql(query)
+        .withClickHouse()
+        .ok(expected);
+  }
+
   @Test void testFloorPostgres() {
     String query = "SELECT floor(\"hire_date\" TO MINUTE) FROM \"employee\"";
     String expected = "SELECT DATE_TRUNC('MINUTE', \"hire_date\")\nFROM 
\"foodmart\".\"employee\"";
@@ -2834,6 +2861,25 @@ class RelToSqlConverterTest {
         .ok(expected);
   }
 
+  @Test public void testFloorWeek() {
+    final String query = "SELECT floor(\"hire_date\" TO WEEK) FROM 
\"employee\"";
+    final String expectedClickHouse = "SELECT toMonday(`hire_date`)\n"
+        + "FROM `foodmart`.`employee`";
+    final String expectedMssql = "SELECT CONVERT(DATETIME, 
CONVERT(VARCHAR(10), "
+        + "DATEADD(day, - (6 + DATEPART(weekday, [hire_date] )) % 7, 
[hire_date] ), 126))\n"
+        + "FROM [foodmart].[employee]";
+    final String expectedMysql = "SELECT STR_TO_DATE(DATE_FORMAT(`hire_date` , 
'%x%v-1'), "
+        + "'%x%v-%w')\n"
+        + "FROM `foodmart`.`employee`";
+    sql(query)
+        .withClickHouse()
+        .ok(expectedClickHouse)
+        .withMssql()
+        .ok(expectedMssql)
+        .withMysql()
+        .ok(expectedMysql);
+  }
+
   @Test void testUnparseSqlIntervalQualifierDb2() {
     String queryDatePlus = "select  * from \"employee\" where  \"hire_date\" + 
"
         + "INTERVAL '19800' SECOND(5) > TIMESTAMP '2005-10-17 00:00:00' ";
@@ -2957,6 +3003,24 @@ class RelToSqlConverterTest {
         .ok(expected);
   }
 
+  @Test public void testFloorMonth() {
+    final String query = "SELECT floor(\"hire_date\" TO MONTH) FROM 
\"employee\"";
+    final String expectedClickHouse = "SELECT toStartOfMonth(`hire_date`)\n"
+        + "FROM `foodmart`.`employee`";
+    final String expectedMssql = "SELECT CONVERT(DATETIME, CONVERT(VARCHAR(7), 
[hire_date] , "
+        + "126)+'-01')\n"
+        + "FROM [foodmart].[employee]";
+    final String expectedMysql = "SELECT DATE_FORMAT(`hire_date`, 
'%Y-%m-01')\n"
+        + "FROM `foodmart`.`employee`";
+    sql(query)
+        .withClickHouse()
+        .ok(expectedClickHouse)
+        .withMssql()
+        .ok(expectedMssql)
+        .withMysql()
+        .ok(expectedMysql);
+  }
+
   @Test void testFloorMysqlHour() {
     String query = "SELECT floor(\"hire_date\" TO HOUR) FROM \"employee\"";
     String expected = "SELECT DATE_FORMAT(`hire_date`, '%Y-%m-%d %H:00:00')\n"
@@ -2994,6 +3058,9 @@ class RelToSqlConverterTest {
     final String expected = "SELECT TRUNC(hire_date, 'MI')\n"
         + "FROM foodmart.employee\n"
         + "GROUP BY TRUNC(hire_date, 'MI')";
+    final String expectedClickHouse = "SELECT toStartOfMinute(`hire_date`)\n"
+        + "FROM `foodmart`.`employee`\n"
+        + "GROUP BY toStartOfMinute(`hire_date`)";
     final String expectedOracle = "SELECT TRUNC(\"hire_date\", 'MINUTE')\n"
         + "FROM \"foodmart\".\"employee\"\n"
         + "GROUP BY TRUNC(\"hire_date\", 'MINUTE')";
@@ -3007,6 +3074,8 @@ class RelToSqlConverterTest {
     sql(query)
         .withHsqldb()
         .ok(expected)
+        .withClickHouse()
+        .ok(expectedClickHouse)
         .withOracle()
         .ok(expectedOracle)
         .withPostgresql()
@@ -3018,6 +3087,8 @@ class RelToSqlConverterTest {
   @Test void testSubstring() {
     final String query = "select substring(\"brand_name\" from 2) "
         + "from \"product\"\n";
+    final String expectedClickHouse = "SELECT substring(`brand_name`, 2)\n"
+        + "FROM `foodmart`.`product`";
     final String expectedOracle = "SELECT SUBSTR(\"brand_name\", 2)\n"
         + "FROM \"foodmart\".\"product\"";
     final String expectedPostgresql = "SELECT SUBSTRING(\"brand_name\" FROM 
2)\n"
@@ -3027,6 +3098,8 @@ class RelToSqlConverterTest {
     final String expectedMysql = "SELECT SUBSTRING(`brand_name` FROM 2)\n"
         + "FROM `foodmart`.`product`";
     sql(query)
+        .withClickHouse()
+        .ok(expectedClickHouse)
         .withOracle()
         .ok(expectedOracle)
         .withPostgresql()
@@ -3045,6 +3118,8 @@ class RelToSqlConverterTest {
   @Test void testSubstringWithFor() {
     final String query = "select substring(\"brand_name\" from 2 for 3) "
         + "from \"product\"\n";
+    final String expectedClickHouse = "SELECT substring(`brand_name`, 2, 3)\n"
+        + "FROM `foodmart`.`product`";
     final String expectedOracle = "SELECT SUBSTR(\"brand_name\", 2, 3)\n"
         + "FROM \"foodmart\".\"product\"";
     final String expectedPostgresql = "SELECT SUBSTRING(\"brand_name\" FROM 2 
FOR 3)\n"
@@ -3056,6 +3131,8 @@ class RelToSqlConverterTest {
     final String expectedMssql = "SELECT SUBSTRING([brand_name], 2, 3)\n"
         + "FROM [foodmart].[product]";
     sql(query)
+        .withClickHouse()
+        .ok(expectedClickHouse)
         .withOracle()
         .ok(expectedOracle)
         .withPostgresql()
@@ -5100,6 +5177,10 @@ class RelToSqlConverterTest {
       return dialect(SqlDialect.DatabaseProduct.CALCITE.getDialect());
     }
 
+    Sql withClickHouse() {
+      return dialect(SqlDialect.DatabaseProduct.CLICKHOUSE.getDialect());
+    }
+
     Sql withDb2() {
       return dialect(SqlDialect.DatabaseProduct.DB2.getDialect());
     }

Reply via email to