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 61b7280ee1167235e3fbd76a6ce379fc83658dbb
Author: yanzhi <[email protected]>
AuthorDate: Thu Jul 11 10:59:07 2019 +0800

    [CALCITE-3175] AssertionError while serializing to JSON a RexLiteral with 
Enum type (Wang Yanlin)
    
    An example of this is the "LEADING" flag in a call to the "TRIM"
    function. "LEADING" is an instance of enum SqlTrimFunction.Flag, and
    becomes a RexLiteral of type SYMBOL.
    
    The solution is to serialize enum values as strings, and build a
    registry of enum classes that may be serialized to JSON and the names
    of their constants. Since the enums have distinct names (for instance,
    no other enum class has a constant called "LEADING"), when we
    deserialize we can use a map to convert them to the correct type.
    
    Close apache/calcite#1301
---
 .../calcite/rel/externalize/RelEnumTypes.java      | 106 +++++++++++++++++++++
 .../apache/calcite/rel/externalize/RelJson.java    |   7 +-
 .../java/org/apache/calcite/rex/RexBuilder.java    |   2 +
 .../java/org/apache/calcite/tools/RelBuilder.java  |   3 +
 .../org/apache/calcite/plan/RelWriterTest.java     |  58 +++++++++++
 5 files changed, 174 insertions(+), 2 deletions(-)

diff --git 
a/core/src/main/java/org/apache/calcite/rel/externalize/RelEnumTypes.java 
b/core/src/main/java/org/apache/calcite/rel/externalize/RelEnumTypes.java
new file mode 100644
index 0000000..43f2d21
--- /dev/null
+++ b/core/src/main/java/org/apache/calcite/rel/externalize/RelEnumTypes.java
@@ -0,0 +1,106 @@
+/*
+ * 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.rel.externalize;
+
+import org.apache.calcite.avatica.util.TimeUnitRange;
+import org.apache.calcite.sql.JoinConditionType;
+import org.apache.calcite.sql.JoinType;
+import org.apache.calcite.sql.SqlExplain;
+import org.apache.calcite.sql.SqlExplainFormat;
+import org.apache.calcite.sql.SqlExplainLevel;
+import org.apache.calcite.sql.SqlInsertKeyword;
+import org.apache.calcite.sql.SqlJsonConstructorNullClause;
+import org.apache.calcite.sql.SqlJsonQueryWrapperBehavior;
+import org.apache.calcite.sql.SqlJsonValueEmptyOrErrorBehavior;
+import org.apache.calcite.sql.SqlMatchRecognize;
+import org.apache.calcite.sql.SqlSelectKeyword;
+import org.apache.calcite.sql.fun.SqlTrimFunction;
+
+import com.google.common.collect.ImmutableMap;
+
+/** Registry of {@link Enum} classes that can be serialized to JSON.
+ *
+ * <p>Suppose you want to serialize the value
+ * {@link SqlTrimFunction.Flag#LEADING} to JSON.
+ * First, make sure that {@link SqlTrimFunction.Flag} is registered.
+ * The type will be serialized as "SYMBOL".
+ * The value will be serialized as the string "LEADING".
+ *
+ * <p>When we deserialize, we rely on the fact that the registered
+ * {@code enum} classes have distinct values. Therefore, knowing that
+ * {@code (type="SYMBOL", value="LEADING")} we can convert the string "LEADING"
+ * to the enum {@code Flag.LEADING}. */
+@SuppressWarnings({"rawtypes", "unchecked"})
+public abstract class RelEnumTypes {
+  private RelEnumTypes() {}
+
+  private static final ImmutableMap<String, Enum<?>> ENUM_BY_NAME;
+
+  static {
+    // Build a mapping from enum constants (e.g. LEADING) to the enum
+    // that contains them (e.g. SqlTrimFunction.Flag). If there two
+    // enum constants have the same name, the builder will throw.
+    final ImmutableMap.Builder<String, Enum<?>> enumByName =
+        ImmutableMap.builder();
+    register(enumByName, JoinConditionType.class);
+    register(enumByName, JoinType.class);
+    register(enumByName, SqlExplain.Depth.class);
+    register(enumByName, SqlExplainFormat.class);
+    register(enumByName, SqlExplainLevel.class);
+    register(enumByName, SqlInsertKeyword.class);
+    register(enumByName, SqlJsonConstructorNullClause.class);
+    register(enumByName, SqlJsonQueryWrapperBehavior.class);
+    register(enumByName, SqlJsonValueEmptyOrErrorBehavior.class);
+    register(enumByName, SqlMatchRecognize.AfterOption.class);
+    register(enumByName, SqlSelectKeyword.class);
+    register(enumByName, SqlTrimFunction.Flag.class);
+    register(enumByName, TimeUnitRange.class);
+    ENUM_BY_NAME = enumByName.build();
+  }
+
+  private static void register(ImmutableMap.Builder<String, Enum<?>> builder,
+      Class<? extends Enum> aClass) {
+    for (Enum enumConstant : aClass.getEnumConstants()) {
+      builder.put(enumConstant.name(), enumConstant);
+    }
+  }
+
+  /** Converts a literal into a value that can be serialized to JSON.
+   * In particular, if is an enum, converts it to its name. */
+  public static Object fromEnum(Object value) {
+    return value instanceof Enum ? fromEnum((Enum) value) : value;
+  }
+
+  /** Converts an enum into its name.
+   * Throws if the enum's class is not registered. */
+  public static String fromEnum(Enum enumValue) {
+    if (ENUM_BY_NAME.get(enumValue.name()) != enumValue) {
+      throw new AssertionError("cannot serialize enum value to JSON: "
+          + enumValue.getDeclaringClass().getCanonicalName() + "."
+          + enumValue);
+    }
+    return enumValue.name();
+  }
+
+  /** Converts a string to an enum value.
+   * The converse of {@link #fromEnum(Enum)}. */
+  static <E extends Enum<E>> E toEnum(String name) {
+    return (E) ENUM_BY_NAME.get(name);
+  }
+}
+
+// End RelEnumTypes.java
diff --git a/core/src/main/java/org/apache/calcite/rel/externalize/RelJson.java 
b/core/src/main/java/org/apache/calcite/rel/externalize/RelJson.java
index a264946..169ecb0 100644
--- a/core/src/main/java/org/apache/calcite/rel/externalize/RelJson.java
+++ b/core/src/main/java/org/apache/calcite/rel/externalize/RelJson.java
@@ -329,7 +329,7 @@ public class RelJson {
       final RexLiteral literal = (RexLiteral) node;
       final Object value = literal.getValue3();
       map = jsonBuilder.map();
-      map.put("literal", value);
+      map.put("literal", RelEnumTypes.fromEnum(value));
       map.put("type", toJson(node.getType()));
       return map;
     case INPUT_REF:
@@ -506,7 +506,7 @@ public class RelJson {
         return rexBuilder.makeCorrel(type, new CorrelationId(correl));
       }
       if (map.containsKey("literal")) {
-        final Object literal = map.get("literal");
+        Object literal = map.get("literal");
         final RelDataType type = toType(typeFactory, map.get("type"));
         if (literal == null) {
           return rexBuilder.makeNullLiteral(type);
@@ -517,6 +517,9 @@ public class RelJson {
           // we just interpret the literal
           return toRex(relInput, literal);
         }
+        if (type.getSqlTypeName() == SqlTypeName.SYMBOL) {
+          literal = RelEnumTypes.toEnum((String) literal);
+        }
         return rexBuilder.makeLiteral(literal, type, false);
       }
       throw new UnsupportedOperationException("cannot convert to rex " + o);
diff --git a/core/src/main/java/org/apache/calcite/rex/RexBuilder.java 
b/core/src/main/java/org/apache/calcite/rex/RexBuilder.java
index 5a0104b..cdb1008 100644
--- a/core/src/main/java/org/apache/calcite/rex/RexBuilder.java
+++ b/core/src/main/java/org/apache/calcite/rex/RexBuilder.java
@@ -1415,6 +1415,8 @@ public class RexBuilder {
     case INTERVAL_SECOND:
       return makeIntervalLiteral((BigDecimal) value,
           type.getIntervalQualifier());
+    case SYMBOL:
+      return makeFlag((Enum) value);
     case MAP:
       final MapSqlType mapType = (MapSqlType) type;
       @SuppressWarnings("unchecked")
diff --git a/core/src/main/java/org/apache/calcite/tools/RelBuilder.java 
b/core/src/main/java/org/apache/calcite/tools/RelBuilder.java
index da28f3f..0ff0c75 100644
--- a/core/src/main/java/org/apache/calcite/tools/RelBuilder.java
+++ b/core/src/main/java/org/apache/calcite/tools/RelBuilder.java
@@ -373,6 +373,9 @@ public class RelBuilder {
           BigDecimal.valueOf(((Number) value).longValue()));
     } else if (value instanceof String) {
       return rexBuilder.makeLiteral((String) value);
+    } else if (value instanceof Enum) {
+      return rexBuilder.makeLiteral(value,
+          getTypeFactory().createSqlType(SqlTypeName.SYMBOL), false);
     } else {
       throw new IllegalArgumentException("cannot convert " + value
           + " (" + value.getClass() + ") to a constant");
diff --git a/core/src/test/java/org/apache/calcite/plan/RelWriterTest.java 
b/core/src/test/java/org/apache/calcite/plan/RelWriterTest.java
index 415292f..4734413 100644
--- a/core/src/test/java/org/apache/calcite/plan/RelWriterTest.java
+++ b/core/src/test/java/org/apache/calcite/plan/RelWriterTest.java
@@ -19,7 +19,9 @@ package org.apache.calcite.plan;
 import org.apache.calcite.adapter.java.ReflectiveSchema;
 import org.apache.calcite.rel.RelCollations;
 import org.apache.calcite.rel.RelNode;
+import org.apache.calcite.rel.RelShuttleImpl;
 import org.apache.calcite.rel.core.AggregateCall;
+import org.apache.calcite.rel.core.TableScan;
 import org.apache.calcite.rel.externalize.RelJsonReader;
 import org.apache.calcite.rel.externalize.RelJsonWriter;
 import org.apache.calcite.rel.logical.LogicalAggregate;
@@ -35,10 +37,15 @@ import org.apache.calcite.sql.SqlExplainFormat;
 import org.apache.calcite.sql.SqlExplainLevel;
 import org.apache.calcite.sql.SqlWindow;
 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.SqlTypeName;
 import org.apache.calcite.test.JdbcTest;
+import org.apache.calcite.test.RelBuilderTest;
+import org.apache.calcite.tools.FrameworkConfig;
 import org.apache.calcite.tools.Frameworks;
+import org.apache.calcite.tools.RelBuilder;
+import org.apache.calcite.util.Holder;
 import org.apache.calcite.util.ImmutableBitSet;
 import org.apache.calcite.util.TestUtil;
 
@@ -472,6 +479,57 @@ public class RelWriterTest {
             + "  LogicalFilter(condition=[=($1, null:INTEGER)])\n"
             + "    LogicalTableScan(table=[[hr, emps]])\n"));
   }
+
+  @Test public void testTrim() {
+    final FrameworkConfig config = RelBuilderTest.config().build();
+    final RelBuilder b = RelBuilder.create(config);
+    final RelNode rel =
+        b.scan("EMP")
+            .project(
+                b.alias(
+                    b.call(SqlStdOperatorTable.TRIM,
+                        b.literal(SqlTrimFunction.Flag.BOTH),
+                        b.literal(" "),
+                        b.field("ENAME")),
+                    "trimmed_ename"))
+            .build();
+
+    RelJsonWriter jsonWriter = new RelJsonWriter();
+    rel.explain(jsonWriter);
+    String relJson = jsonWriter.asString();
+    final RelOptSchema schema = getSchema(rel);
+    final String s =
+        Frameworks.withPlanner((cluster, relOptSchema, rootSchema) -> {
+          final RelJsonReader reader =
+              new RelJsonReader(cluster, schema, rootSchema);
+          RelNode node;
+          try {
+            node = reader.read(relJson);
+          } catch (IOException e) {
+            throw TestUtil.rethrow(e);
+          }
+          return RelOptUtil.dumpPlan("", node, SqlExplainFormat.TEXT,
+              SqlExplainLevel.EXPPLAN_ATTRIBUTES);
+        });
+    final String expected = ""
+        + "LogicalProject(trimmed_ename=[TRIM(FLAG(BOTH), ' ', $1)])\n"
+        + "  LogicalTableScan(table=[[scott, EMP]])\n";
+    assertThat(s, isLinux(expected));
+  }
+
+  /** Returns the schema of a {@link org.apache.calcite.rel.core.TableScan}
+   * in this plan, or null if there are no scans. */
+  private RelOptSchema getSchema(RelNode rel) {
+    final Holder<RelOptSchema> schemaHolder = Holder.of(null);
+    rel.accept(
+        new RelShuttleImpl() {
+          @Override public RelNode visit(TableScan scan) {
+            schemaHolder.set(scan.getTable().getRelOptSchema());
+            return super.visit(scan);
+          }
+        });
+    return schemaHolder.get();
+  }
 }
 
 // End RelWriterTest.java

Reply via email to