Skip to content

Commit

Permalink
[CALCITE-3175] AssertionError while serializing to JSON a RexLiteral …
Browse files Browse the repository at this point in the history
…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#1301
  • Loading branch information
yanlin-Lynn authored and julianhyde committed Aug 3, 2019
1 parent f82353c commit 61b7280
Show file tree
Hide file tree
Showing 5 changed files with 174 additions and 2 deletions.
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
Expand Up @@ -329,7 +329,7 @@ private Object toJson(RexNode node) {
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:
Expand Down Expand Up @@ -506,7 +506,7 @@ RexNode toRex(RelInput relInput, Object o) {
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);
Expand All @@ -517,6 +517,9 @@ RexNode toRex(RelInput relInput, Object o) {
// 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);
Expand Down
2 changes: 2 additions & 0 deletions core/src/main/java/org/apache/calcite/rex/RexBuilder.java
Original file line number Diff line number Diff line change
Expand Up @@ -1415,6 +1415,8 @@ public RexNode makeLiteral(Object value, RelDataType type,
case INTERVAL_SECOND:
return makeIntervalLiteral((BigDecimal) value,
type.getIntervalQualifier());
case SYMBOL:
return makeFlag((Enum) value);
case MAP:
final MapSqlType mapType = (MapSqlType) type;
@SuppressWarnings("unchecked")
Expand Down
3 changes: 3 additions & 0 deletions core/src/main/java/org/apache/calcite/tools/RelBuilder.java
Original file line number Diff line number Diff line change
Expand Up @@ -373,6 +373,9 @@ public RexNode literal(Object value) {
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");
Expand Down
58 changes: 58 additions & 0 deletions core/src/test/java/org/apache/calcite/plan/RelWriterTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,9 @@
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;
Expand All @@ -35,10 +37,15 @@
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;

Expand Down Expand Up @@ -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

0 comments on commit 61b7280

Please sign in to comment.