You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@calcite.apache.org by jh...@apache.org on 2023/04/28 06:21:16 UTC
[calcite] 03/03: [CALCITE-5614] Serialize Sarg values to and from JSON
This is an automated email from the ASF dual-hosted git repository.
jhyde pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/calcite.git
commit 3326475c766267d521330006cc80730c4e456191
Author: Oliver Lee <ol...@google.com>
AuthorDate: Tue Mar 14 18:04:47 2023 -0700
[CALCITE-5614] Serialize Sarg values to and from JSON
Close apache/calcite#3140
Co-authored-by: Oliver Lee <ol...@google.com>
Co-authored-by: Julian Hyde <jh...@apache.org>
---
.../calcite/rel/externalize/RelEnumTypes.java | 2 +
.../apache/calcite/rel/externalize/RelJson.java | 237 ++++++++++++++++++++-
.../calcite/rel/externalize/RelJsonReader.java | 11 +
.../java/org/apache/calcite/rex/RexBuilder.java | 9 +-
.../java/org/apache/calcite/sql/SqlCollation.java | 11 +-
.../java/org/apache/calcite/util/DateString.java | 8 +-
.../java/org/apache/calcite/util/JsonBuilder.java | 2 +-
.../java/org/apache/calcite/util/NlsString.java | 18 +-
.../java/org/apache/calcite/util/RangeSets.java | 110 ++++++++--
.../java/org/apache/calcite/util/TimeString.java | 6 +-
.../org/apache/calcite/plan/RelWriterTest.java | 95 ++++++++-
.../java/org/apache/calcite/util/RangeSetTest.java | 57 +++++
12 files changed, 526 insertions(+), 40 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
index 97181d035d..b4ad2d1b90 100644
--- a/core/src/main/java/org/apache/calcite/rel/externalize/RelEnumTypes.java
+++ b/core/src/main/java/org/apache/calcite/rel/externalize/RelEnumTypes.java
@@ -18,6 +18,7 @@ package org.apache.calcite.rel.externalize;
import org.apache.calcite.avatica.util.TimeUnitRange;
import org.apache.calcite.rel.core.TableModify;
+import org.apache.calcite.rex.RexUnknownAs;
import org.apache.calcite.sql.JoinConditionType;
import org.apache.calcite.sql.JoinType;
import org.apache.calcite.sql.SqlExplain;
@@ -66,6 +67,7 @@ public abstract class RelEnumTypes {
ImmutableMap.builder();
register(enumByName, JoinConditionType.class);
register(enumByName, JoinType.class);
+ register(enumByName, RexUnknownAs.class);
register(enumByName, SqlExplain.Depth.class);
register(enumByName, SqlExplainFormat.class);
register(enumByName, SqlExplainLevel.class);
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 c3f8a7c2ca..0987f1f288 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
@@ -17,6 +17,7 @@
package org.apache.calcite.rel.externalize;
import org.apache.calcite.avatica.AvaticaUtils;
+import org.apache.calcite.avatica.util.ByteString;
import org.apache.calcite.avatica.util.TimeUnit;
import org.apache.calcite.plan.RelOptCluster;
import org.apache.calcite.plan.RelOptTable;
@@ -62,13 +63,25 @@ import org.apache.calcite.sql.fun.SqlStdOperatorTable;
import org.apache.calcite.sql.parser.SqlParserPos;
import org.apache.calcite.sql.type.SqlTypeName;
import org.apache.calcite.sql.validate.SqlNameMatchers;
+import org.apache.calcite.util.DateString;
import org.apache.calcite.util.ImmutableBitSet;
import org.apache.calcite.util.ImmutableIntList;
import org.apache.calcite.util.JsonBuilder;
+import org.apache.calcite.util.NlsString;
+import org.apache.calcite.util.RangeSets;
+import org.apache.calcite.util.Sarg;
+import org.apache.calcite.util.TimeString;
import org.apache.calcite.util.Util;
+import com.fasterxml.jackson.core.JsonProcessingException;
+import com.fasterxml.jackson.databind.DeserializationFeature;
+import com.fasterxml.jackson.databind.ObjectMapper;
import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableRangeSet;
+import com.google.common.collect.Range;
+import com.google.common.collect.RangeSet;
+import org.checkerframework.checker.nullness.qual.NonNull;
import org.checkerframework.checker.nullness.qual.Nullable;
import org.checkerframework.checker.nullness.qual.PolyNull;
@@ -93,6 +106,14 @@ import static java.util.Objects.requireNonNull;
* into JSON format.
*/
public class RelJson {
+ private static final ObjectMapper OBJECT_MAPPER =
+ new ObjectMapper()
+ .configure(DeserializationFeature.USE_BIG_DECIMAL_FOR_FLOATS, true);
+
+ private static final List<Class> VALUE_CLASSES =
+ ImmutableList.of(NlsString.class, BigDecimal.class, ByteString.class,
+ Boolean.class, DateString.class, TimeString.class);
+
private final Map<String, Constructor> constructorMap = new HashMap<>();
private final @Nullable JsonBuilder jsonBuilder;
private final InputTranslator inputTranslator;
@@ -422,6 +443,7 @@ public class RelJson {
return map;
}
+ @SuppressWarnings({"BetaApi", "UnstableApiUsage"}) // RangeSet GA in Guava 32
public @Nullable Object toJson(@Nullable Object value) {
if (value == null
|| value instanceof Number
@@ -460,12 +482,47 @@ public class RelJson {
return toJson((RelDataTypeField) value);
} else if (value instanceof RelDistribution) {
return toJson((RelDistribution) value);
+ } else if (value instanceof Sarg) {
+ //noinspection unchecked,rawtypes
+ return toJson((Sarg) value);
+ } else if (value instanceof RangeSet) {
+ //noinspection unchecked,rawtypes
+ return toJson((RangeSet) value);
+ } else if (value instanceof Range) {
+ //noinspection rawtypes,unchecked
+ return toJson((Range) value);
} else {
throw new UnsupportedOperationException("type not serializable: "
+ value + " (type " + value.getClass().getCanonicalName() + ")");
}
}
+ public <C extends Comparable<C>> Object toJson(Sarg<C> node) {
+ final Map<String, @Nullable Object> map = jsonBuilder().map();
+ map.put("rangeSet", toJson(node.rangeSet));
+ map.put("nullAs", RelEnumTypes.fromEnum(node.nullAs));
+ return map;
+ }
+
+ @SuppressWarnings({"BetaApi", "UnstableApiUsage"}) // RangeSet GA in Guava 32
+ public <C extends Comparable<C>> List<List<String>> toJson(
+ RangeSet<C> rangeSet) {
+ final List<List<String>> list = new ArrayList<>();
+ try {
+ RangeSets.forEach(rangeSet,
+ RangeToJsonConverter.<C>instance().andThen(list::add));
+ } catch (Exception e) {
+ throw new RuntimeException("Failed to serialize RangeSet: ", e);
+ }
+ return list;
+ }
+
+ /** Serializes a {@link Range} that can be deserialized using
+ * {@link RelJson#rangeFromJson(List)}. */
+ public <C extends Comparable<C>> List<String> toJson(Range<C> range) {
+ return RangeSets.map(range, RangeToJsonConverter.instance());
+ }
+
private Object toJson(RelDataType node) {
final Map<String, @Nullable Object> map = jsonBuilder().map();
if (node.isStruct()) {
@@ -517,7 +574,7 @@ public class RelJson {
return node.getId();
}
- private Object toJson(RexNode node) {
+ public Object toJson(RexNode node) {
final Map<String, @Nullable Object> map;
switch (node.getKind()) {
case FIELD_ACCESS:
@@ -530,7 +587,11 @@ public class RelJson {
final RexLiteral literal = (RexLiteral) node;
final Object value = literal.getValue3();
map = jsonBuilder().map();
- map.put("literal", RelEnumTypes.fromEnum(value));
+ //noinspection rawtypes
+ map.put("literal",
+ value instanceof Enum
+ ? RelEnumTypes.fromEnum((Enum) value)
+ : toJson(value));
map.put("type", toJson(node.getType()));
return map;
case INPUT_REF:
@@ -657,7 +718,8 @@ public class RelJson {
final RexBuilder rexBuilder = cluster.getRexBuilder();
if (o == null) {
return null;
- } else if (o instanceof Map) {
+ // Support JSON deserializing of non-default Map classes such as gson LinkedHashMap
+ } else if (Map.class.isAssignableFrom(o.getClass())) {
final Map<String, @Nullable Object> map = (Map) o;
final RelDataTypeFactory typeFactory = cluster.getTypeFactory();
if (map.containsKey("op")) {
@@ -746,11 +808,26 @@ public class RelJson {
return toRex(relInput, literal);
}
final RelDataType type = toType(typeFactory, get(map, "type"));
+ if (literal instanceof Map
+ && ((Map<?, ?>) literal).containsKey("rangeSet")) {
+ Sarg sarg = sargFromJson((Map) literal);
+ return rexBuilder.makeSearchArgumentLiteral(sarg, type);
+ }
if (type.getSqlTypeName() == SqlTypeName.SYMBOL) {
literal = RelEnumTypes.toEnum((String) literal);
}
return rexBuilder.makeLiteral(literal, type);
}
+ if (map.containsKey("sargLiteral")) {
+ Object sargObject = map.get("sargLiteral");
+ if (sargObject == null) {
+ final RelDataType type = toType(typeFactory, get(map, "type"));
+ return rexBuilder.makeNullLiteral(type);
+ }
+ final RelDataType type = toType(typeFactory, get(map, "type"));
+ Sarg sarg = sargFromJson((Map) sargObject);
+ return rexBuilder.makeSearchArgumentLiteral(sarg, type);
+ }
throw new UnsupportedOperationException("cannot convert to rex " + o);
} else if (o instanceof Boolean) {
return rexBuilder.makeLiteral((Boolean) o);
@@ -770,8 +847,91 @@ public class RelJson {
}
}
- private void addRexFieldCollationList(
- List<RexFieldCollation> list,
+ /** Converts a JSON object to a {@code Sarg}.
+ *
+ * <p>For example,
+ * {@code {rangeSet: [["[", 0, 5, "]"], ["[", 10, "-", ")"]],
+ * nullAs: "UNKNOWN"}} represents the range x ≥ 0 and x ≤ 5 or
+ * x > 10.
+ */
+ // BetaApi is no longer a concern; the Beta tag was removed in Guava 32.0
+ @SuppressWarnings({"BetaApi", "unchecked"})
+ public static <C extends Comparable<C>> Sarg<C> sargFromJson(
+ Map<String, Object> map) {
+ final String nullAs = requireNonNull((String) map.get("nullAs"), "nullAs");
+ final List<List<String>> rangeSet =
+ requireNonNull((List<List<String>>) map.get("rangeSet"), "rangeSet");
+ return Sarg.of(RelEnumTypes.toEnum(nullAs),
+ RelJson.<C>rangeSetFromJson(rangeSet));
+ }
+
+ /** Converts a JSON list to a {@link RangeSet}. */
+ @SuppressWarnings({"BetaApi", "UnstableApiUsage"}) // RangeSet GA in Guava 32
+ public static <C extends Comparable<C>> RangeSet<C> rangeSetFromJson(
+ List<List<String>> rangeSetsJson) {
+ final ImmutableRangeSet.Builder<C> builder = ImmutableRangeSet.builder();
+ try {
+ rangeSetsJson.forEach(list -> builder.add(rangeFromJson(list)));
+ } catch (Exception e) {
+ throw new RuntimeException("Error creating RangeSet from JSON: ", e);
+ }
+ return builder.build();
+ }
+
+ /** Creates a {@link Range} from a JSON object.
+ *
+ * <p>The JSON object is as serialized using {@link RelJson#toJson(Range)},
+ * e.g. {@code ["[", ")", 10, "-"]}.
+ *
+ * @see RangeToJsonConverter */
+ public static <C extends Comparable<C>> Range<C> rangeFromJson(
+ List<String> list) {
+ switch (list.get(0)) {
+ case "all":
+ return Range.all();
+ case "atLeast":
+ return Range.atLeast(rangeEndPointFromJson(list.get(1)));
+ case "atMost":
+ return Range.atMost(rangeEndPointFromJson(list.get(1)));
+ case "greaterThan":
+ return Range.greaterThan(rangeEndPointFromJson(list.get(1)));
+ case "lessThan":
+ return Range.lessThan(rangeEndPointFromJson(list.get(1)));
+ case "singleton":
+ return Range.singleton(rangeEndPointFromJson(list.get(1)));
+ case "closed":
+ return Range.closed(rangeEndPointFromJson(list.get(1)),
+ rangeEndPointFromJson(list.get(2)));
+ case "closedOpen":
+ return Range.closedOpen(rangeEndPointFromJson(list.get(1)),
+ rangeEndPointFromJson(list.get(2)));
+ case "openClosed":
+ return Range.openClosed(rangeEndPointFromJson(list.get(1)),
+ rangeEndPointFromJson(list.get(2)));
+ case "open":
+ return Range.open(rangeEndPointFromJson(list.get(1)),
+ rangeEndPointFromJson(list.get(2)));
+ default:
+ throw new AssertionError("unknown range type " + list.get(0));
+ }
+ }
+
+ @SuppressWarnings({"rawtypes", "unchecked"})
+ private static <C extends Comparable<C>> C rangeEndPointFromJson(Object o) {
+ Exception e = null;
+ for (Class clsType : VALUE_CLASSES) {
+ try {
+ return (C) OBJECT_MAPPER.readValue((String) o, clsType);
+ } catch (JsonProcessingException ex) {
+ e = ex;
+ }
+ }
+ throw new RuntimeException(
+ "Error deserializing range endpoint (did not find compatible type): ",
+ e);
+ }
+
+ private void addRexFieldCollationList(List<RexFieldCollation> list,
RelInput relInput, @Nullable List<Map<String, Object>> order) {
if (order == null) {
return;
@@ -1005,4 +1165,71 @@ public class RelJson {
RexNode translateInput(RelJson relJson, int input,
Map<String, @Nullable Object> map, RelInput relInput);
}
+
+ /** Implementation of {@link RangeSets.Handler} that converts a {@link Range}
+ * event to a list of strings.
+ *
+ * @param <V> Range value type
+ */
+ private static class RangeToJsonConverter<V>
+ implements RangeSets.Handler<@NonNull V, List<String>> {
+ @SuppressWarnings("rawtypes")
+ private static final RangeToJsonConverter INSTANCE =
+ new RangeToJsonConverter<>();
+
+ private static <C extends Comparable<C>> RangeToJsonConverter<C> instance() {
+ //noinspection unchecked
+ return INSTANCE;
+ }
+
+ @Override public List<String> all() {
+ return ImmutableList.of("all");
+ }
+
+ @Override public List<String> atLeast(@NonNull V lower) {
+ return ImmutableList.of("atLeast", toJson(lower));
+ }
+
+ @Override public List<String> atMost(@NonNull V upper) {
+ return ImmutableList.of("atMost", toJson(upper));
+ }
+
+ @Override public List<String> greaterThan(@NonNull V lower) {
+ return ImmutableList.of("greaterThan", toJson(lower));
+ }
+
+ @Override public List<String> lessThan(@NonNull V upper) {
+ return ImmutableList.of("lessThan", toJson(upper));
+ }
+
+ @Override public List<String> singleton(@NonNull V value) {
+ return ImmutableList.of("singleton", toJson(value));
+ }
+
+ @Override public List<String> closed(@NonNull V lower, @NonNull V upper) {
+ return ImmutableList.of("closed", toJson(lower), toJson(upper));
+ }
+
+ @Override public List<String> closedOpen(@NonNull V lower,
+ @NonNull V upper) {
+ return ImmutableList.of("closedOpen", toJson(lower), toJson(upper));
+ }
+
+ @Override public List<String> openClosed(@NonNull V lower,
+ @NonNull V upper) {
+ return ImmutableList.of("openClosed", toJson(lower), toJson(upper));
+ }
+
+ @Override public List<String> open(@NonNull V lower, @NonNull V upper) {
+ return ImmutableList.of("open", toJson(lower), toJson(upper));
+ }
+
+ private static String toJson(Object o) {
+ try {
+ return OBJECT_MAPPER.writeValueAsString(o);
+ } catch (JsonProcessingException e) {
+ throw new RuntimeException("Failed to serialize Range endpoint: ", e);
+ }
+ }
+ }
}
diff --git a/core/src/main/java/org/apache/calcite/rel/externalize/RelJsonReader.java b/core/src/main/java/org/apache/calcite/rel/externalize/RelJsonReader.java
index a98d0e8848..8b6d1e0f03 100644
--- a/core/src/main/java/org/apache/calcite/rel/externalize/RelJsonReader.java
+++ b/core/src/main/java/org/apache/calcite/rel/externalize/RelJsonReader.java
@@ -109,6 +109,17 @@ public class RelJsonReader {
return RelJson.create().toType(typeFactory, o);
}
+ /** Converts a JSON string (such as that produced by
+ * {@link RelJson#toJson(RexNode)}) into a Calcite expression. */
+ public static RexNode readRex(RelOptCluster typeFactory, String s)
+ throws IOException {
+ final ObjectMapper mapper = new ObjectMapper();
+ Map<String, Object> o = mapper
+ .configure(DeserializationFeature.USE_BIG_DECIMAL_FOR_FLOATS, true)
+ .readValue(s, TYPE_REF);
+ return RelJson.create().toRex(typeFactory, o);
+ }
+
private void readRels(List<Map<String, Object>> jsonRels) {
for (Map<String, Object> jsonRel : jsonRels) {
readRel(jsonRel);
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 c19a1001c6..fda55faa0c 100644
--- a/core/src/main/java/org/apache/calcite/rex/RexBuilder.java
+++ b/core/src/main/java/org/apache/calcite/rex/RexBuilder.java
@@ -1638,6 +1638,10 @@ public class RexBuilder {
case INTEGER:
case BIGINT:
case DECIMAL:
+ if (value instanceof RexLiteral
+ && ((RexLiteral) value).getTypeName() == SqlTypeName.SARG) {
+ return (RexNode) value;
+ }
return makeExactLiteral((BigDecimal) value, type);
case FLOAT:
case REAL:
@@ -1736,10 +1740,13 @@ public class RexBuilder {
* {@link org.apache.calcite.rex.RexLiteral#valueMatchesType}.
*
* <p>Returns null if and only if {@code o} is null. */
- private static @PolyNull Object clean(@PolyNull Object o, RelDataType type) {
+ private @PolyNull Object clean(@PolyNull Object o, RelDataType type) {
if (o == null) {
return o;
}
+ if (o instanceof Sarg) {
+ return makeSearchArgumentLiteral((Sarg) o, type);
+ }
switch (type.getSqlTypeName()) {
case TINYINT:
case SMALLINT:
diff --git a/core/src/main/java/org/apache/calcite/sql/SqlCollation.java b/core/src/main/java/org/apache/calcite/sql/SqlCollation.java
index b6576d4edc..b02035f791 100644
--- a/core/src/main/java/org/apache/calcite/sql/SqlCollation.java
+++ b/core/src/main/java/org/apache/calcite/sql/SqlCollation.java
@@ -22,6 +22,10 @@ import org.apache.calcite.util.Glossary;
import org.apache.calcite.util.SerializableCharset;
import org.apache.calcite.util.Util;
+import com.fasterxml.jackson.annotation.JsonCreator;
+import com.fasterxml.jackson.annotation.JsonIgnore;
+import com.fasterxml.jackson.annotation.JsonProperty;
+
import org.checkerframework.checker.initialization.qual.UnderInitialization;
import org.checkerframework.checker.nullness.qual.Nullable;
import org.checkerframework.dataflow.qual.Pure;
@@ -95,9 +99,10 @@ public class SqlCollation implements Serializable {
* @param collation Collation specification
* @param coercibility Coercibility
*/
+ @JsonCreator
public SqlCollation(
- String collation,
- Coercibility coercibility) {
+ @JsonProperty("collationName") String collation,
+ @JsonProperty("coercibility") Coercibility coercibility) {
this.coercibility = coercibility;
SqlParserUtil.ParsedCollation parseValues =
SqlParserUtil.parseCollation(collation);
@@ -290,6 +295,7 @@ public class SqlCollation implements Serializable {
writer.identifier(collationName, false);
}
+ @JsonIgnore
public Charset getCharset() {
return wrappedCharset.getCharset();
}
@@ -312,6 +318,7 @@ public class SqlCollation implements Serializable {
* which case {@link String#compareTo} will be used.
*/
@Pure
+ @JsonIgnore
public @Nullable Collator getCollator() {
return null;
}
diff --git a/core/src/main/java/org/apache/calcite/util/DateString.java b/core/src/main/java/org/apache/calcite/util/DateString.java
index 33e3675353..2dc19b5def 100644
--- a/core/src/main/java/org/apache/calcite/util/DateString.java
+++ b/core/src/main/java/org/apache/calcite/util/DateString.java
@@ -18,6 +18,9 @@ package org.apache.calcite.util;
import org.apache.calcite.avatica.util.DateTimeUtils;
+import com.fasterxml.jackson.annotation.JsonCreator;
+import com.fasterxml.jackson.annotation.JsonIgnore;
+import com.fasterxml.jackson.annotation.JsonProperty;
import com.google.common.base.Preconditions;
import org.checkerframework.checker.nullness.qual.Nullable;
@@ -120,12 +123,15 @@ public class DateString implements Comparable<DateString> {
}
/** Creates a DateString that is a given number of days since the epoch. */
- public static DateString fromDaysSinceEpoch(int days) {
+ @JsonCreator
+ public static DateString fromDaysSinceEpoch(
+ @JsonProperty("daysSinceEpoch") int days) {
return new DateString(DateTimeUtils.unixDateToString(days));
}
/** Returns the number of milliseconds since the epoch. Always a multiple of
* 86,400,000 (the number of milliseconds in a day). */
+ @JsonIgnore
public long getMillisSinceEpoch() {
return getDaysSinceEpoch() * DateTimeUtils.MILLIS_PER_DAY;
}
diff --git a/core/src/main/java/org/apache/calcite/util/JsonBuilder.java b/core/src/main/java/org/apache/calcite/util/JsonBuilder.java
index 68496fb161..eeb59699cf 100644
--- a/core/src/main/java/org/apache/calcite/util/JsonBuilder.java
+++ b/core/src/main/java/org/apache/calcite/util/JsonBuilder.java
@@ -130,7 +130,7 @@ public class JsonBuilder {
} else if (o instanceof String) {
appendString(buf, (String) o);
} else {
- assert o instanceof Number || o instanceof Boolean;
+ assert o instanceof Number || o instanceof Boolean : o;
buf.append(o);
}
}
diff --git a/core/src/main/java/org/apache/calcite/util/NlsString.java b/core/src/main/java/org/apache/calcite/util/NlsString.java
index 327b699fc0..cbc87b6755 100644
--- a/core/src/main/java/org/apache/calcite/util/NlsString.java
+++ b/core/src/main/java/org/apache/calcite/util/NlsString.java
@@ -23,6 +23,8 @@ import org.apache.calcite.sql.SqlDialect;
import org.apache.calcite.sql.SqlUtil;
import org.apache.calcite.sql.dialect.AnsiSqlDialect;
+import com.fasterxml.jackson.annotation.JsonCreator;
+import com.fasterxml.jackson.annotation.JsonProperty;
import com.google.common.cache.CacheBuilder;
import com.google.common.cache.CacheLoader;
import com.google.common.cache.LoadingCache;
@@ -42,6 +44,8 @@ import java.util.Objects;
import static org.apache.calcite.util.Static.RESOURCE;
+import static java.util.Objects.requireNonNull;
+
/**
* A string, optionally with {@link Charset character set} and
* {@link SqlCollation}. It is immutable.
@@ -72,6 +76,7 @@ public class NlsString implements Comparable<NlsString>, Cloneable {
});
private final @Nullable String stringValue;
+ @JsonProperty("valueBytes")
private final @Nullable ByteString bytesValue;
private final @Nullable String charsetName;
private final @Nullable Charset charset;
@@ -94,8 +99,8 @@ public class NlsString implements Comparable<NlsString>, Cloneable {
*/
public NlsString(ByteString bytesValue, String charsetName,
@Nullable SqlCollation collation) {
- this(null, Objects.requireNonNull(bytesValue, "bytesValue"),
- Objects.requireNonNull(charsetName, "charsetName"), collation);
+ this(null, requireNonNull(bytesValue, "bytesValue"),
+ requireNonNull(charsetName, "charsetName"), collation);
}
/**
@@ -111,9 +116,12 @@ public class NlsString implements Comparable<NlsString>, Cloneable {
* @throws RuntimeException If the given value cannot be represented in the
* given charset
*/
- public NlsString(String stringValue, @Nullable String charsetName,
- @Nullable SqlCollation collation) {
- this(Objects.requireNonNull(stringValue, "stringValue"), null, charsetName, collation);
+ @JsonCreator
+ public NlsString(@JsonProperty("value") String stringValue,
+ @JsonProperty("charsetName") @Nullable String charsetName,
+ @JsonProperty("collation") @Nullable SqlCollation collation) {
+ this(requireNonNull(stringValue, "stringValue"), null, charsetName,
+ collation);
}
/** Internal constructor; other constructors must call it. */
diff --git a/core/src/main/java/org/apache/calcite/util/RangeSets.java b/core/src/main/java/org/apache/calcite/util/RangeSets.java
index 70b81686d4..a0967e2383 100644
--- a/core/src/main/java/org/apache/calcite/util/RangeSets.java
+++ b/core/src/main/java/org/apache/calcite/util/RangeSets.java
@@ -22,11 +22,15 @@ import com.google.common.collect.Range;
import com.google.common.collect.RangeSet;
import com.google.common.collect.TreeRangeSet;
+import org.checkerframework.checker.nullness.qual.NonNull;
+
import java.util.Iterator;
import java.util.Set;
import java.util.function.BiConsumer;
import java.util.function.Function;
+import static java.util.Objects.requireNonNull;
+
/** Utilities for Guava {@link com.google.common.collect.RangeSet}. */
@SuppressWarnings({"BetaApi", "UnstableApiUsage"})
public class RangeSets {
@@ -279,39 +283,101 @@ public class RangeSets {
/** Deconstructor for {@link Range} values.
*
- * @param <C> Value type
+ * @param <V> Value type
* @param <R> Return type
*
* @see Consumer */
- public interface Handler<C extends Comparable<C>, R> {
+ public interface Handler<V, R> {
R all();
- R atLeast(C lower);
- R atMost(C upper);
- R greaterThan(C lower);
- R lessThan(C upper);
- R singleton(C value);
- R closed(C lower, C upper);
- R closedOpen(C lower, C upper);
- R openClosed(C lower, C upper);
- R open(C lower, C upper);
+ R atLeast(V lower);
+ R atMost(V upper);
+ R greaterThan(V lower);
+ R lessThan(V upper);
+ R singleton(V value);
+ R closed(V lower, V upper);
+ R closedOpen(V lower, V upper);
+ R openClosed(V lower, V upper);
+ R open(V lower, V upper);
+
+ /** Creates a Consumer that sends output to a given sink. */
+ default Consumer<V> andThen(java.util.function.Consumer<R> consumer) {
+ return new SinkConsumer<>(this, consumer);
+ }
+ }
+
+ /** Consumer that deconstructs a range to a handler then sends the resulting
+ * range to a {@link java.util.function.Consumer}.
+ *
+ * @param <V> Value type
+ * @param <R> Output element type
+ */
+ private static class SinkConsumer<V, R> implements Consumer<V> {
+ final Handler<V, R> handler;
+ final java.util.function.Consumer<R> consumer;
+
+ SinkConsumer(Handler<V, R> handler,
+ java.util.function.Consumer<R> consumer) {
+ this.handler = requireNonNull(handler, "handler");
+ this.consumer = requireNonNull(consumer, "consumer");
+ }
+
+ @Override public void all() {
+ consumer.accept(handler.all());
+ }
+
+ @Override public void atLeast(V lower) {
+ consumer.accept(handler.atLeast(lower));
+ }
+
+ @Override public void atMost(V upper) {
+ consumer.accept(handler.atMost(upper));
+ }
+
+ @Override public void greaterThan(V lower) {
+ consumer.accept(handler.greaterThan(lower));
+ }
+
+ @Override public void lessThan(V upper) {
+ consumer.accept(handler.lessThan(upper));
+ }
+
+ @Override public void singleton(V value) {
+ consumer.accept(handler.singleton(value));
+ }
+
+ @Override public void closed(V lower, V upper) {
+ consumer.accept(handler.closed(lower, upper));
+ }
+
+ @Override public void closedOpen(V lower, V upper) {
+ consumer.accept(handler.closedOpen(lower, upper));
+ }
+
+ @Override public void openClosed(V lower, V upper) {
+ consumer.accept(handler.openClosed(lower, upper));
+ }
+
+ @Override public void open(V lower, V upper) {
+ consumer.accept(handler.open(lower, upper));
+ }
}
/** Consumer of {@link Range} values.
*
- * @param <C> Value type
+ * @param <V> Value type
*
* @see Handler */
- public interface Consumer<C extends Comparable<C>> {
+ public interface Consumer<@NonNull V> {
void all();
- void atLeast(C lower);
- void atMost(C upper);
- void greaterThan(C lower);
- void lessThan(C upper);
- void singleton(C value);
- void closed(C lower, C upper);
- void closedOpen(C lower, C upper);
- void openClosed(C lower, C upper);
- void open(C lower, C upper);
+ void atLeast(V lower);
+ void atMost(V upper);
+ void greaterThan(V lower);
+ void lessThan(V upper);
+ void singleton(V value);
+ void closed(V lower, V upper);
+ void closedOpen(V lower, V upper);
+ void openClosed(V lower, V upper);
+ void open(V lower, V upper);
}
/** Handler that converts a Range into another Range of the same type,
diff --git a/core/src/main/java/org/apache/calcite/util/TimeString.java b/core/src/main/java/org/apache/calcite/util/TimeString.java
index e0d46b837a..249c955030 100644
--- a/core/src/main/java/org/apache/calcite/util/TimeString.java
+++ b/core/src/main/java/org/apache/calcite/util/TimeString.java
@@ -18,6 +18,8 @@ package org.apache.calcite.util;
import org.apache.calcite.avatica.util.DateTimeUtils;
+import com.fasterxml.jackson.annotation.JsonCreator;
+import com.fasterxml.jackson.annotation.JsonProperty;
import com.google.common.base.Preconditions;
import com.google.common.base.Strings;
@@ -146,7 +148,8 @@ public class TimeString implements Comparable<TimeString> {
.withMillis(calendar.get(Calendar.MILLISECOND));
}
- public static TimeString fromMillisOfDay(int i) {
+ @JsonCreator
+ public static TimeString fromMillisOfDay(@JsonProperty("millisOfDay") int i) {
return new TimeString(DateTimeUtils.unixTimeToString(i))
.withMillis((int) floorMod(i, 1000L));
}
@@ -163,7 +166,6 @@ public class TimeString implements Comparable<TimeString> {
}
return new TimeString(v);
}
-
public int getMillisOfDay() {
int h = Integer.valueOf(v.substring(0, 2));
int m = Integer.valueOf(v.substring(3, 5));
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 6c5275ec3f..c493d51ed0 100644
--- a/core/src/test/java/org/apache/calcite/plan/RelWriterTest.java
+++ b/core/src/test/java/org/apache/calcite/plan/RelWriterTest.java
@@ -47,6 +47,7 @@ import org.apache.calcite.rex.RexBuilder;
import org.apache.calcite.rex.RexCorrelVariable;
import org.apache.calcite.rex.RexFieldCollation;
import org.apache.calcite.rex.RexInputRef;
+import org.apache.calcite.rex.RexLiteral;
import org.apache.calcite.rex.RexNode;
import org.apache.calcite.rex.RexProgramBuilder;
import org.apache.calcite.rex.RexWindowBounds;
@@ -65,10 +66,13 @@ import org.apache.calcite.test.schemata.hr.HrSchema;
import org.apache.calcite.tools.FrameworkConfig;
import org.apache.calcite.tools.Frameworks;
import org.apache.calcite.tools.RelBuilder;
+import org.apache.calcite.util.DateString;
import org.apache.calcite.util.Holder;
import org.apache.calcite.util.ImmutableBitSet;
import org.apache.calcite.util.JsonBuilder;
+import org.apache.calcite.util.NlsString;
import org.apache.calcite.util.TestUtil;
+import org.apache.calcite.util.TimeString;
import org.apache.calcite.util.TimestampString;
import com.fasterxml.jackson.core.JsonProcessingException;
@@ -93,6 +97,7 @@ import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
+import java.util.function.Consumer;
import java.util.function.Function;
import java.util.stream.Stream;
@@ -826,6 +831,94 @@ class RelWriterTest {
.assertThatPlan(isLinux(expected));
}
+ @Test void testSearchOperator() {
+ final FrameworkConfig config = RelBuilderTest.config().build();
+ final RelBuilder b = RelBuilder.create(config);
+ final RexBuilder rexBuilder = b.getRexBuilder();
+
+ // Test toJson -> toRex -> toJson is the same.
+ final JsonBuilder jsonBuilder = new JsonBuilder();
+ final RelJson relJson = RelJson.create().withJsonBuilder(jsonBuilder);
+ final Consumer<RexNode> consumer = node -> {
+ Object jsonRepresentation = relJson.toJson(node);
+ assertThat(jsonRepresentation, notNullValue());
+
+ RexNode deserialized = relJson.toRex(b.getCluster(), jsonRepresentation);
+ assertThat(node, is(deserialized));
+ assertThat(jsonRepresentation, is(relJson.toJson(deserialized)));
+
+ // Test that toRex is the same as toJsonString -> readRex
+ final String s = jsonBuilder.toJsonString(jsonRepresentation);
+ RexNode deserialized2;
+ try {
+ deserialized2 = RelJsonReader.readRex(b.getCluster(), s);
+ } catch (IOException e) {
+ throw new RuntimeException(e);
+ }
+ assertThat(deserialized2, is(deserialized));
+ };
+
+ // Commented out but we should also get this passing! SEARCH in a RelNode
+ // using the JSON writer also leads to failures.
+ if (false) {
+ final RelNode rel = b
+ .scan("EMP")
+ .project(b.between(b.field("DEPTNO"), b.literal(20), b.literal(30)))
+ .build();
+ final RelJsonWriter jsonWriter =
+ new RelJsonWriter(new JsonBuilder(), RelJson::withLibraryOperatorTable);
+ rel.explain(jsonWriter);
+ String relJsonString = jsonWriter.asString();
+ String result = deserializeAndDumpToTextFormat(getSchema(rel), relJsonString);
+ final String expected = "<TODO>";
+ assertThat(result, isLinux(expected));
+ }
+
+ RexNode between =
+ rexBuilder.makeBetween(b.literal(45),
+ b.literal(20),
+ b.literal(30));
+ consumer.accept(between);
+
+ RexNode inNode =
+ rexBuilder.makeIn(b.literal(12),
+ ImmutableList.of(
+ b.literal(20),
+ b.literal(14)));
+ consumer.accept(inNode);
+
+ // Test Calcite DateString class works in a Range
+ final DateString d1 =
+ DateString.fromCalendarFields(
+ new TimestampString(1970, 2, 1, 1, 1, 0).toCalendar());
+ final DateString d2 = DateString.fromDaysSinceEpoch(100);
+ final DateString d3 = DateString.fromDaysSinceEpoch(1000);
+ RexNode dateNode =
+ rexBuilder.makeBetween(rexBuilder.makeDateLiteral(d2),
+ rexBuilder.makeDateLiteral(d1),
+ rexBuilder.makeDateLiteral(d3));
+ consumer.accept(dateNode);
+
+ // Test Calcite TimeString
+ final RexLiteral t1 = rexBuilder.makeTimeLiteral(new TimeString(1, 0, 0), 0);
+ final RexLiteral t2 = rexBuilder.makeTimeLiteral(new TimeString(2, 2, 2), 6);
+ final RexLiteral t3 = rexBuilder.makeTimeLiteral(new TimeString(3, 3, 3), 9);
+
+ RexNode timeNode = rexBuilder.makeBetween(t2, t1, t3);
+ consumer.accept(timeNode);
+
+ // Test Calcite NlsString
+ final NlsString nls1 = new NlsString("one", null, null);
+ final NlsString nls2 = new NlsString("ten", null, null);
+ final NlsString nls3 = new NlsString("sixteen", null, null);
+ RexNode nlsNode =
+ rexBuilder.makeIn(
+ rexBuilder.makeCharLiteral(nls2),
+ ImmutableList.of(rexBuilder.makeCharLiteral(nls1),
+ rexBuilder.makeCharLiteral(nls3)));
+ consumer.accept(nlsNode);
+ }
+
@ParameterizedTest
@MethodSource("explainFormats")
void testAggregateWithAlias(SqlExplainFormat format) {
@@ -872,7 +965,7 @@ class RelWriterTest {
/** Test case for
* <a href="https://issues.apache.org/jira/browse/CALCITE-4804">[CALCITE-4804]
- * Support Snapshot operator serialization and deserizalization</a>. */
+ * Support Snapshot operator serialization and deserialization</a>. */
@Test void testSnapshot() {
// Equivalent SQL:
// SELECT *
diff --git a/core/src/test/java/org/apache/calcite/util/RangeSetTest.java b/core/src/test/java/org/apache/calcite/util/RangeSetTest.java
index 904979ae38..5de76de31e 100644
--- a/core/src/test/java/org/apache/calcite/util/RangeSetTest.java
+++ b/core/src/test/java/org/apache/calcite/util/RangeSetTest.java
@@ -17,6 +17,7 @@
package org.apache.calcite.util;
import org.apache.calcite.linq4j.Ord;
+import org.apache.calcite.rel.externalize.RelJson;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableRangeSet;
@@ -27,6 +28,7 @@ import com.google.common.collect.TreeRangeSet;
import org.junit.jupiter.api.Test;
+import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
@@ -44,6 +46,61 @@ import static org.hamcrest.MatcherAssert.assertThat;
*/
@SuppressWarnings("UnstableApiUsage")
class RangeSetTest {
+
+ /** Tests {@link org.apache.calcite.rel.externalize.RelJson#toJson(Range)}
+ * and {@link RelJson#rangeFromJson(List)}. */
+ @Test void testRangeSetSerializeDeserialize() {
+ RelJson relJson = RelJson.create();
+ final Range<BigDecimal> point = Range.singleton(BigDecimal.valueOf(0));
+ final Range<BigDecimal> closedRange1 =
+ Range.closed(BigDecimal.valueOf(0), BigDecimal.valueOf(5));
+ final Range<BigDecimal> closedRange2 =
+ Range.closed(BigDecimal.valueOf(6), BigDecimal.valueOf(10));
+
+ final Range<BigDecimal> gt1 = Range.greaterThan(BigDecimal.valueOf(7));
+ final Range<BigDecimal> al1 = Range.atLeast(BigDecimal.valueOf(8));
+ final Range<BigDecimal> lt1 = Range.lessThan(BigDecimal.valueOf(4));
+ final Range<BigDecimal> am1 = Range.atMost(BigDecimal.valueOf(3));
+
+ // Test serialize/deserialize Range
+ // Point
+ assertThat(RelJson.rangeFromJson(relJson.toJson(point)), is(point));
+ // Closed Range
+ assertThat(RelJson.rangeFromJson(relJson.toJson(closedRange1)),
+ is(closedRange1));
+ // Open Range
+ assertThat(RelJson.rangeFromJson(relJson.toJson(gt1)), is(gt1));
+ assertThat(RelJson.rangeFromJson(relJson.toJson(al1)), is(al1));
+ assertThat(RelJson.rangeFromJson(relJson.toJson(lt1)), is(lt1));
+ assertThat(RelJson.rangeFromJson(relJson.toJson(am1)), is(am1));
+ // Test closed single RangeSet
+ final RangeSet<BigDecimal> closedRangeSet = ImmutableRangeSet.of(closedRange1);
+ assertThat(RelJson.rangeSetFromJson(relJson.toJson(closedRangeSet)),
+ is(closedRangeSet));
+ // Test complex RangeSets
+ final RangeSet<BigDecimal> complexClosedRangeSet1 =
+ ImmutableRangeSet.<BigDecimal>builder()
+ .add(closedRange1)
+ .add(closedRange2)
+ .build();
+ assertThat(
+ RelJson.rangeSetFromJson(relJson.toJson(complexClosedRangeSet1)),
+ is(complexClosedRangeSet1));
+ final RangeSet<BigDecimal> complexClosedRangeSet2 =
+ ImmutableRangeSet.<BigDecimal>builder()
+ .add(gt1)
+ .add(am1)
+ .build();
+ assertThat(RelJson.rangeSetFromJson(relJson.toJson(complexClosedRangeSet2)),
+ is(complexClosedRangeSet2));
+
+ // Test None and All
+ final RangeSet<BigDecimal> setNone = ImmutableRangeSet.of();
+ final RangeSet<BigDecimal> setAll = setNone.complement();
+ assertThat(RelJson.rangeSetFromJson(relJson.toJson(setNone)), is(setNone));
+ assertThat(RelJson.rangeSetFromJson(relJson.toJson(setAll)), is(setAll));
+ }
+
/** Tests {@link RangeSets#minus(RangeSet, Range)}. */
@SuppressWarnings("UnstableApiUsage")
@Test void testRangeSetMinus() {