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

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


The following commit(s) were added to refs/heads/main by this push:
     new e536d39496 [CALCITE-7081] Invalid unparse for cast to nested type in 
ClickHouse
e536d39496 is described below

commit e536d3949674cbbc0fd1162b3fccb211ca75789b
Author: xuzifu666 <[email protected]>
AuthorDate: Mon Jun 30 11:43:29 2025 +0800

    [CALCITE-7081] Invalid unparse for cast to nested type in ClickHouse
---
 .../calcite/sql/dialect/ClickHouseSqlDialect.java  |  68 +++++++-------
 .../apache/calcite/util/RelToSqlConverterUtil.java | 100 +++++++++++++++++++++
 .../calcite/rel/rel2sql/RelToSqlConverterTest.java |  51 +++++++++++
 3 files changed, 188 insertions(+), 31 deletions(-)

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
index 630ef0cb74..c4bd7802bf 100644
--- 
a/core/src/main/java/org/apache/calcite/sql/dialect/ClickHouseSqlDialect.java
+++ 
b/core/src/main/java/org/apache/calcite/sql/dialect/ClickHouseSqlDialect.java
@@ -35,7 +35,6 @@
 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 org.apache.calcite.util.RelToSqlConverterUtil;
 
@@ -118,36 +117,43 @@ public ClickHouseSqlDialect(Context context) {
   }
 
   @Override public @Nullable SqlNode getCastSpec(RelDataType type) {
-    if (type instanceof BasicSqlType) {
-      SqlTypeName typeName = type.getSqlTypeName();
-      switch (typeName) {
-      case CHAR:
-        return createSqlDataTypeSpecByName(
-            String.format(Locale.ROOT, "FixedString(%s)",
-            type.getPrecision()), typeName, type.isNullable());
-      case VARCHAR:
-        return createSqlDataTypeSpecByName("String", typeName, 
type.isNullable());
-      case TINYINT:
-        return createSqlDataTypeSpecByName("Int8", typeName, 
type.isNullable());
-      case SMALLINT:
-        return createSqlDataTypeSpecByName("Int16", typeName, 
type.isNullable());
-      case INTEGER:
-        return createSqlDataTypeSpecByName("Int32", typeName, 
type.isNullable());
-      case BIGINT:
-        return createSqlDataTypeSpecByName("Int64", typeName, 
type.isNullable());
-      case REAL:
-        return createSqlDataTypeSpecByName("Float32", typeName, 
type.isNullable());
-      case FLOAT:
-      case DOUBLE:
-        return createSqlDataTypeSpecByName("Float64", typeName, 
type.isNullable());
-      case DATE:
-        return createSqlDataTypeSpecByName("Date", typeName, 
type.isNullable());
-      case TIMESTAMP:
-      case TIMESTAMP_WITH_LOCAL_TIME_ZONE:
-        return createSqlDataTypeSpecByName("DateTime", typeName, 
type.isNullable());
-      default:
-        break;
-      }
+    SqlTypeName typeName = type.getSqlTypeName();
+    switch (typeName) {
+    case CHAR:
+      return createSqlDataTypeSpecByName(
+          String.format(Locale.ROOT, "FixedString(%s)",
+              type.getPrecision()), typeName, type.isNullable());
+    case VARCHAR:
+      return createSqlDataTypeSpecByName("String", typeName, 
type.isNullable());
+    case TINYINT:
+      return createSqlDataTypeSpecByName("Int8", typeName, type.isNullable());
+    case SMALLINT:
+      return createSqlDataTypeSpecByName("Int16", typeName, type.isNullable());
+    case INTEGER:
+      return createSqlDataTypeSpecByName("Int32", typeName, type.isNullable());
+    case BIGINT:
+      return createSqlDataTypeSpecByName("Int64", typeName, type.isNullable());
+    case REAL:
+      return createSqlDataTypeSpecByName("Float32", typeName, 
type.isNullable());
+    case FLOAT:
+    case DOUBLE:
+      return createSqlDataTypeSpecByName("Float64", typeName, 
type.isNullable());
+    case DATE:
+      return createSqlDataTypeSpecByName("Date", typeName, type.isNullable());
+    case TIMESTAMP:
+    case TIMESTAMP_WITH_LOCAL_TIME_ZONE:
+      return createSqlDataTypeSpecByName("DateTime", typeName, 
type.isNullable());
+    case MAP:
+      return RelToSqlConverterUtil.getCastSpecClickHouseSqlMapType(this, type,
+          SqlParserPos.ZERO);
+    case ARRAY:
+      return RelToSqlConverterUtil.getCastSpecClickHouseSqlArrayType(this, 
type,
+          SqlParserPos.ZERO);
+    case MULTISET:
+      throw new UnsupportedOperationException("ClickHouse dialect does not 
support cast to "
+          + type.getSqlTypeName());
+    default:
+      break;
     }
 
     return super.getCastSpec(type);
diff --git 
a/core/src/main/java/org/apache/calcite/util/RelToSqlConverterUtil.java 
b/core/src/main/java/org/apache/calcite/util/RelToSqlConverterUtil.java
index 15fa6fe24c..b165448615 100644
--- a/core/src/main/java/org/apache/calcite/util/RelToSqlConverterUtil.java
+++ b/core/src/main/java/org/apache/calcite/util/RelToSqlConverterUtil.java
@@ -16,20 +16,29 @@
  */
 package org.apache.calcite.util;
 
+import org.apache.calcite.rel.type.RelDataType;
 import org.apache.calcite.rex.RexBuilder;
 import org.apache.calcite.rex.RexCall;
 import org.apache.calcite.rex.RexNode;
 import org.apache.calcite.rex.RexUtil;
 import org.apache.calcite.sql.SqlCall;
 import org.apache.calcite.sql.SqlCharStringLiteral;
+import org.apache.calcite.sql.SqlCollectionTypeNameSpec;
+import org.apache.calcite.sql.SqlDataTypeSpec;
+import org.apache.calcite.sql.SqlDialect;
 import org.apache.calcite.sql.SqlKind;
 import org.apache.calcite.sql.SqlLiteral;
+import org.apache.calcite.sql.SqlMapTypeNameSpec;
 import org.apache.calcite.sql.SqlNode;
 import org.apache.calcite.sql.SqlSpecialOperator;
+import org.apache.calcite.sql.SqlTypeNameSpec;
 import org.apache.calcite.sql.SqlWriter;
 import org.apache.calcite.sql.fun.SqlStdOperatorTable;
 import org.apache.calcite.sql.fun.SqlTrimFunction;
 import org.apache.calcite.sql.parser.SqlParserPos;
+import org.apache.calcite.sql.type.ArraySqlType;
+import org.apache.calcite.sql.type.MapSqlType;
+import org.apache.calcite.sql.type.SqlTypeName;
 
 import static org.apache.calcite.sql.fun.SqlLibraryOperators.REGEXP_REPLACE_3;
 
@@ -328,4 +337,95 @@ public static void unparseBoolLiteralToCondition(SqlWriter 
writer, boolean value
     writer.literal(value ? "1" : "0");
     writer.endList(frame);
   }
+
+  /**
+   * Transformation Map type from {@code MAP<VARCHAR,VARCHAR>} to {@code 
Map(VARCHAR,VARCHAR)}.
+   */
+  public static SqlDataTypeSpec getCastSpecClickHouseSqlMapType(SqlDialect 
dialect,
+      RelDataType type, SqlParserPos pos) {
+    MapSqlType mapSqlType = (MapSqlType) type;
+    SqlDataTypeSpec keySpec = (SqlDataTypeSpec) 
dialect.getCastSpec(mapSqlType.getKeyType());
+    SqlDataTypeSpec valueSpec =
+        (SqlDataTypeSpec) dialect.getCastSpec(mapSqlType.getValueType());
+    SqlDataTypeSpec nonNullKeySpec =
+        requireNonNull(keySpec, "keySpec");
+    SqlDataTypeSpec nonNullValueSpec =
+        requireNonNull(valueSpec, "valueSpec");
+    SqlMapTypeNameSpec sqlMapTypeNameSpec =
+        new ClickHouseSqlMapTypeNameSpec(nonNullKeySpec, nonNullValueSpec, 
pos);
+    return new SqlDataTypeSpec(sqlMapTypeNameSpec,
+        SqlParserPos.ZERO);
+  }
+
+  /**
+   * Transformation Map type from {@code VARCHAR ARRAY} to {@code 
Array(VARCHAR)}.
+   */
+  public static SqlDataTypeSpec getCastSpecClickHouseSqlArrayType(SqlDialect 
dialect,
+      RelDataType type, SqlParserPos pos) {
+    ArraySqlType arraySqlType = (ArraySqlType) type;
+    SqlDataTypeSpec arrayValueSpec =
+        (SqlDataTypeSpec) dialect.getCastSpec(arraySqlType.getComponentType());
+    SqlDataTypeSpec nonNullarrayValueSpec =
+        requireNonNull(arrayValueSpec, "arrayValueSpec");
+    ClickHouseSqlArrayTypeNameSpec sqlArrayTypeNameSpec =
+        new 
ClickHouseSqlArrayTypeNameSpec(nonNullarrayValueSpec.getTypeNameSpec(),
+            arraySqlType.getSqlTypeName(), pos);
+    return new SqlDataTypeSpec(sqlArrayTypeNameSpec, SqlParserPos.ZERO);
+  }
+
+  /**
+   * ClickHouseSqlMapTypeNameSpec to parse or unparse SQL MAP type to {@code 
Map(VARCHAR, VARCHAR)}.
+   */
+  public static class ClickHouseSqlMapTypeNameSpec extends SqlMapTypeNameSpec {
+
+    /**
+     * Creates a {@code SqlMapTypeNameSpec}.
+     * example: MAP type would convert to Map(VARCHAR, VARCHAR).
+     *
+     * @param keyType key type of the Map
+     * @param valType value type of the Map
+     * @param pos the parser position, must not be null
+     */
+    public ClickHouseSqlMapTypeNameSpec(SqlDataTypeSpec keyType,
+        SqlDataTypeSpec valType, SqlParserPos pos) {
+      super(keyType, valType, pos);
+    }
+
+    @Override public void unparse(SqlWriter writer, int leftPrec, int 
rightPrec) {
+      writer.print("Map");
+      SqlWriter.Frame frame =
+          writer.startList(SqlWriter.FrameTypeEnum.FUN_CALL, "(", ")");
+      writer.sep(","); // configures the writer
+      getKeyType().unparse(writer, leftPrec, rightPrec);
+      writer.sep(",");
+      getValType().unparse(writer, leftPrec, rightPrec);
+      writer.endList(frame);
+    }
+  }
+
+  /**
+   * A ClickHouseSqlArrayTypeNameSpec to parse or unparse SQL ARRAY type to 
{@code Array(VARCHAR)}.
+   */
+  public static class ClickHouseSqlArrayTypeNameSpec extends 
SqlCollectionTypeNameSpec {
+
+    /**
+     * Creates a {@code ClickHouseSqlArrayTypeNameSpec}.
+     * example: integer array would convert to Array(integer).
+     *
+     * @param elementTypeName    Type of the collection element
+     * @param collectionTypeName Collection type name
+     * @param pos                Parser position, must not be null
+     */
+    public ClickHouseSqlArrayTypeNameSpec(SqlTypeNameSpec elementTypeName,
+        SqlTypeName collectionTypeName, SqlParserPos pos) {
+      super(elementTypeName, collectionTypeName, pos);
+    }
+
+    @Override public void unparse(SqlWriter writer, int leftPrec, int 
rightPrec) {
+      writer.print("Array");
+      SqlWriter.Frame frame = 
writer.startList(SqlWriter.FrameTypeEnum.FUN_CALL, "(", ")");
+      this.getElementTypeName().unparse(writer, leftPrec, rightPrec);
+      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 b400b85a04..59a4147deb 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
@@ -2601,6 +2601,56 @@ private SqlDialect nonOrdinalDialect() {
     sql(query4).withStarRocks().ok(expectedStarRocks4);
   }
 
+  /** Test case for
+   * <a 
href="https://issues.apache.org/jira/browse/CALCITE-7081";>[CALCITE-7081]
+   * Invalid unparse for cast to nested type in ClickHouse</a>.
+   */
+  @Test void testCastNestedClickHouse() {
+    // All converted sql had been test passed in ClickHouse env.
+    final String query = "select cast(array['a','b','c']"
+        + " as varchar array)";
+    final String expectedClickHouse =
+        "SELECT CAST(array('a', 'b', 'c') AS Array(`String`))";
+    sql(query).withClickHouse().ok(expectedClickHouse);
+
+    final String query0 = "select cast(array['a','b','c',null]"
+        + " as varchar array)";
+    final String expectedClickHouse0 =
+        "SELECT CAST(array('a', 'b', 'c', NULL) AS Array(`Nullable(String)`))";
+    sql(query0).withClickHouse().ok(expectedClickHouse0);
+
+    final String query1 = "select cast(array[array['a'], array['b'], 
array['c']]"
+        + " as varchar array array)";
+    final String expectedClickHouse1 =
+        "SELECT CAST(array(array('a'), array('b'), array('c')) AS 
Array(Array(`String`)))";
+    sql(query1).withClickHouse().ok(expectedClickHouse1);
+
+    final String query2 = "select 
cast(array[MAP['a','1'],MAP['b','2'],MAP['c','3']]"
+        + " as MAP<varchar,varchar> array)";
+    final String expectedClickHouse2 =
+        "SELECT CAST(array(map('a', '1'), map('b', '2'), map('c', '3'))"
+            + " AS Array(Map(`String`, `Nullable(String)`)))";
+    sql(query2).withClickHouse().ok(expectedClickHouse2);
+
+    final String query3 = "select cast(MAP['a',ARRAY[1,2,3]]"
+        + " as MAP<varchar,integer array>)";
+    final String expectedClickHouse3 =
+        "SELECT CAST(map('a', array(1, 2, 3)) AS Map(`String`, 
Array(`Nullable(Int32)`)))";
+    sql(query3).withClickHouse().ok(expectedClickHouse3);
+
+    final String query4 = "select cast(MAP['a',ARRAY[1.0,2.0,3.0]]"
+        + " as MAP<varchar,real array>)";
+    final String expectedClickHouse4 =
+        "SELECT CAST(map('a', array(1.0, 2.0, 3.0)) AS Map(`String`, 
Array(`Nullable(Float32)`)))";
+    sql(query4).withClickHouse().ok(expectedClickHouse4);
+
+    final String query5 = "select cast(MAP['a',MAP['b','c']]"
+        + " as MAP<varchar,MAP<varchar,varchar>>)";
+    final String expectedClickHouse5 =
+        "SELECT CAST(map('a', map('b', 'c')) AS Map(`String`, Map(`String`, 
`Nullable(String)`)))";
+    sql(query5).withClickHouse().ok(expectedClickHouse5);
+  }
+
   /** Test case for
    * <a 
href="https://issues.apache.org/jira/browse/CALCITE-6088";>[CALCITE-6088]
    * SqlItemOperator fails in RelToSqlConverter</a>. */
@@ -10206,6 +10256,7 @@ private void checkLiteral2(String expression, String 
expected) {
     sql(query2)
         .withPhoenix().throws_("Phoenix dialect does not support cast to 
MULTISET")
         .withStarRocks().throws_("StarRocks dialect does not support cast to 
MULTISET")
+        .withClickHouse().throws_("ClickHouse dialect does not support cast to 
MULTISET")
         .withHive().throws_("Hive dialect does not support cast to MULTISET");
 
     String query3 = "SELECT CAST(MAP[1.0,2.0,3.0,4.0] AS MAP<FLOAT, REAL>) 
FROM \"employee\"";

Reply via email to