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 &ge; 0 and x &le; 5 or
+   * x &gt; 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() {