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
