[CALCITE-4446] Implement three-valued logic for SEARCH operator

Close apache/calcite#2357
diff --git a/core/src/main/java/org/apache/calcite/rel/rel2sql/SqlImplementor.java b/core/src/main/java/org/apache/calcite/rel/rel2sql/SqlImplementor.java
index 5ae29ef..4632853 100644
--- a/core/src/main/java/org/apache/calcite/rel/rel2sql/SqlImplementor.java
+++ b/core/src/main/java/org/apache/calcite/rel/rel2sql/SqlImplementor.java
@@ -47,6 +47,7 @@
 import org.apache.calcite.rex.RexProgram;
 import org.apache.calcite.rex.RexShuttle;
 import org.apache.calcite.rex.RexSubQuery;
+import org.apache.calcite.rex.RexUnknownAs;
 import org.apache.calcite.rex.RexWindow;
 import org.apache.calcite.rex.RexWindowBound;
 import org.apache.calcite.sql.JoinType;
@@ -892,7 +893,7 @@
         RexNode operand, RelDataType type, Sarg<C> sarg) {
       final List<SqlNode> orList = new ArrayList<>();
       final SqlNode operandSql = toSql(program, operand);
-      if (sarg.containsNull) {
+      if (sarg.nullAs == RexUnknownAs.TRUE) {
         orList.add(SqlStdOperatorTable.IS_NULL.createCall(POS, operandSql));
       }
       if (sarg.isPoints()) {
diff --git a/core/src/main/java/org/apache/calcite/rex/RexAnalyzer.java b/core/src/main/java/org/apache/calcite/rex/RexAnalyzer.java
index 65afaec..9d46f83 100644
--- a/core/src/main/java/org/apache/calcite/rex/RexAnalyzer.java
+++ b/core/src/main/java/org/apache/calcite/rex/RexAnalyzer.java
@@ -131,11 +131,14 @@
     }
 
     @Override public Void visitCall(RexCall call) {
-      if (!RexInterpreter.SUPPORTED_SQL_KIND.contains(call.getKind())) {
+      switch (call.getKind()) {
+      case CAST:
+      case OTHER_FUNCTION:
         ++unsupportedCount;
         return null;
+      default:
+        return super.visitCall(call);
       }
-      return super.visitCall(call);
     }
   }
 }
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 7bd3536..6a6e599 100644
--- a/core/src/main/java/org/apache/calcite/rex/RexBuilder.java
+++ b/core/src/main/java/org/apache/calcite/rex/RexBuilder.java
@@ -1330,7 +1330,7 @@
    * otherwise creates a disjunction, "arg = point0 OR arg = point1 OR ...". */
   public RexNode makeIn(RexNode arg, List<? extends RexNode> ranges) {
     if (areAssignable(arg, ranges)) {
-      final Sarg sarg = toSarg(Comparable.class, ranges, false);
+      final Sarg sarg = toSarg(Comparable.class, ranges, RexUnknownAs.UNKNOWN);
       if (sarg != null) {
         final RexNode range0 = ranges.get(0);
         return makeCall(SqlStdOperatorTable.SEARCH,
@@ -1369,7 +1369,7 @@
         && upperValue != null
         && areAssignable(arg, Arrays.asList(lower, upper))) {
       final Sarg sarg =
-          Sarg.of(false,
+          Sarg.of(RexUnknownAs.UNKNOWN,
               ImmutableRangeSet.<Comparable>of(
                   Range.closed(lowerValue, upperValue)));
       return makeCall(SqlStdOperatorTable.SEARCH, arg,
@@ -1384,7 +1384,7 @@
    * not possible. */
   @SuppressWarnings({"BetaApi", "UnstableApiUsage"})
   private static <C extends Comparable<C>> @Nullable Sarg<C> toSarg(Class<C> clazz,
-      List<? extends RexNode> ranges, boolean containsNull) {
+      List<? extends RexNode> ranges, RexUnknownAs unknownAs) {
     if (ranges.isEmpty()) {
       // Cannot convert an empty list to a Sarg (by this interface, at least)
       // because we use the type of the first element.
@@ -1398,7 +1398,7 @@
       }
       rangeSet.add(Range.singleton(value));
     }
-    return Sarg.of(containsNull, rangeSet);
+    return Sarg.of(unknownAs, rangeSet);
   }
 
   private static <C extends Comparable<C>> @Nullable C toComparable(Class<C> clazz,
diff --git a/core/src/main/java/org/apache/calcite/rex/RexCall.java b/core/src/main/java/org/apache/calcite/rex/RexCall.java
index 5347a10..2526d7d 100644
--- a/core/src/main/java/org/apache/calcite/rex/RexCall.java
+++ b/core/src/main/java/org/apache/calcite/rex/RexCall.java
@@ -214,7 +214,8 @@
     case SEARCH:
       final Sarg sarg = ((RexLiteral) operands.get(1)).getValueAs(Sarg.class);
       return requireNonNull(sarg, "sarg").isAll()
-          && (sarg.containsNull || !operands.get(0).getType().isNullable());
+          && (sarg.nullAs == RexUnknownAs.TRUE
+              || !operands.get(0).getType().isNullable());
     default:
       return false;
     }
@@ -235,7 +236,8 @@
     case SEARCH:
       final Sarg sarg = ((RexLiteral) operands.get(1)).getValueAs(Sarg.class);
       return requireNonNull(sarg, "sarg").isNone()
-          && (!sarg.containsNull || !operands.get(0).getType().isNullable());
+          && (sarg.nullAs == RexUnknownAs.FALSE
+              || !operands.get(0).getType().isNullable());
     default:
       return false;
     }
diff --git a/core/src/main/java/org/apache/calcite/rex/RexInterpreter.java b/core/src/main/java/org/apache/calcite/rex/RexInterpreter.java
index ac234e2..8a4012e 100644
--- a/core/src/main/java/org/apache/calcite/rex/RexInterpreter.java
+++ b/core/src/main/java/org/apache/calcite/rex/RexInterpreter.java
@@ -20,11 +20,19 @@
 import org.apache.calcite.avatica.util.TimeUnit;
 import org.apache.calcite.avatica.util.TimeUnitRange;
 import org.apache.calcite.rel.metadata.NullSentinel;
+import org.apache.calcite.runtime.SqlFunctions;
 import org.apache.calcite.sql.SqlKind;
+import org.apache.calcite.sql.type.SqlTypeName;
+import org.apache.calcite.util.DateString;
 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.TimestampString;
 import org.apache.calcite.util.Util;
 
 import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.RangeSet;
 
 import org.checkerframework.checker.nullness.qual.Nullable;
 
@@ -209,6 +217,10 @@
       return ceil(call, values);
     case EXTRACT:
       return extract(values);
+    case LIKE:
+      return like(values);
+    case SEARCH:
+      return search(call.operands.get(1).getType().getSqlTypeName(), values);
     default:
       throw unbound(call);
     }
@@ -231,6 +243,58 @@
     return DateTimeUtils.unixDateExtract(timeUnitRange, v2);
   }
 
+  private static Comparable like(List<Comparable> values) {
+    if (containsNull(values)) {
+      return N;
+    }
+    final NlsString value = (NlsString) values.get(0);
+    final NlsString pattern = (NlsString) values.get(1);
+    switch (values.size()) {
+    case 2:
+      return SqlFunctions.like(value.getValue(), pattern.getValue());
+    case 3:
+      final NlsString escape = (NlsString) values.get(2);
+      return SqlFunctions.like(value.getValue(), pattern.getValue(),
+          escape.getValue());
+    default:
+      throw new AssertionError();
+    }
+  }
+
+  @SuppressWarnings({"BetaApi", "rawtypes", "unchecked", "UnstableApiUsage"})
+  private static Comparable search(SqlTypeName typeName, List<Comparable> values) {
+    final Comparable value = values.get(0);
+    final Sarg sarg = (Sarg) values.get(1);
+    if (value == N) {
+      switch (sarg.nullAs) {
+      case FALSE:
+        return false;
+      case TRUE:
+        return true;
+      default:
+        return N;
+      }
+    }
+    return translate(sarg.rangeSet, typeName).contains(value);
+  }
+
+  /** Translates the values in a RangeSet from literal format to runtime format.
+   * For example the DATE SQL type uses DateString for literals and Integer at
+   * runtime. */
+  @SuppressWarnings({"BetaApi", "rawtypes", "unchecked", "UnstableApiUsage"})
+  private static RangeSet translate(RangeSet rangeSet, SqlTypeName typeName) {
+    switch (typeName) {
+    case DATE:
+      return RangeSets.copy(rangeSet, DateString::getDaysSinceEpoch);
+    case TIME:
+      return RangeSets.copy(rangeSet, TimeString::getMillisOfDay);
+    case TIMESTAMP:
+      return RangeSets.copy(rangeSet, TimestampString::getMillisSinceEpoch);
+    default:
+      return rangeSet;
+    }
+  }
+
   private static Comparable coalesce(List<Comparable> values) {
     for (Comparable value : values) {
       if (value != N) {
diff --git a/core/src/main/java/org/apache/calcite/rex/RexSimplify.java b/core/src/main/java/org/apache/calcite/rex/RexSimplify.java
index a070734..0579025 100644
--- a/core/src/main/java/org/apache/calcite/rex/RexSimplify.java
+++ b/core/src/main/java/org/apache/calcite/rex/RexSimplify.java
@@ -336,7 +336,12 @@
     if (e.operands.get(1) instanceof RexLiteral) {
       final RexLiteral literal = (RexLiteral) e.operands.get(1);
       if ("%".equals(literal.getValueAs(String.class))) {
-        return rexBuilder.makeLiteral(true);
+        RexNode x = e.operands.get(0);
+        // "x LIKE '%'" simplifies to "UNKNOWN OR x IS NOT NULL"
+        return simplify(
+            rexBuilder.makeCall(SqlStdOperatorTable.OR,
+                rexBuilder.makeNullLiteral(e.getType()),
+                rexBuilder.makeCall(SqlStdOperatorTable.IS_NOT_NULL, x)));
       }
     }
     return simplifyGenericNode(e);
@@ -1254,7 +1259,7 @@
 
     // prepare all condition/branches for boolean interpretation
     // It's done here make these interpretation changes available to case2or simplifications
-    // but not interfere with the normal simplifcation recursion
+    // but not interfere with the normal simplification recursion
     List<CaseBranch> branches = new ArrayList<>();
     for (CaseBranch branch : inputBranches) {
       if ((branches.size() > 0 && !isSafeExpression(branch.cond))
@@ -1342,9 +1347,10 @@
 
     final SargCollector sargCollector = new SargCollector(rexBuilder, true);
     operands.forEach(t -> sargCollector.accept(t, terms));
-    if (sargCollector.needToFix(unknownAs)) {
+    if (sargCollector.needToFix()) {
       operands.clear();
-      terms.forEach(t -> operands.add(sargCollector.fix(rexBuilder, t)));
+      terms.forEach(t ->
+          operands.add(SargCollector.fix(rexBuilder, t, unknownAs)));
     }
     terms.clear();
 
@@ -1816,9 +1822,10 @@
     final SargCollector sargCollector = new SargCollector(rexBuilder, false);
     final List<RexNode> newTerms = new ArrayList<>();
     terms.forEach(t -> sargCollector.accept(t, newTerms));
-    if (sargCollector.needToFix(unknownAs)) {
+    if (sargCollector.needToFix()) {
       terms.clear();
-      newTerms.forEach(t -> terms.add(sargCollector.fix(rexBuilder, t)));
+      newTerms.forEach(t ->
+          terms.add(SargCollector.fix(rexBuilder, t, unknownAs)));
     }
 
     // CALCITE-3198 Auxiliary map to simplify cases like:
@@ -1952,9 +1959,10 @@
         }
       }
       if (!v0.equals(v1)) {
-        throw new AssertionError("result mismatch: when applied to " + map
-            + ", " + before + " yielded " + v0
-            + ", and " + simplified + " yielded " + v1);
+        throw new AssertionError("result mismatch (unknown as "
+            + unknownAs + "): when applied to " + map + ",\n"
+            + before + " yielded " + v0 + ";\n"
+            + simplified + " yielded " + v1);
       }
     }
   }
@@ -1965,22 +1973,19 @@
     if (call.getOperands().get(1) instanceof RexLiteral) {
       RexLiteral literal = (RexLiteral) call.getOperands().get(1);
       final Sarg sarg = castNonNull(literal.getValueAs(Sarg.class));
-      if (sarg.isAll()
-          && (sarg.containsNull || !a.getType().isNullable())) {
-        // SEARCH(x, [TRUE])  -> TRUE
-        // SEARCH(x, [NOT NULL])  -> TRUE if x's type is NOT NULL
-        return rexBuilder.makeLiteral(true);
+      if (sarg.isAll() || sarg.isNone()) {
+        return RexUtil.simpleSarg(rexBuilder, a, sarg, unknownAs);
       }
       // Remove null from sarg if the left-hand side is never null
-      if (sarg.containsNull) {
+      if (sarg.nullAs != UNKNOWN) {
         final RexNode simplified = simplifyIs1(SqlKind.IS_NULL, a, unknownAs);
         if (simplified != null
             && simplified.isAlwaysFalse()) {
-          final Sarg sarg2 = Sarg.of(false, sarg.rangeSet);
+          final Sarg sarg2 = Sarg.of(UNKNOWN, sarg.rangeSet);
           final RexLiteral literal2 =
               rexBuilder.makeLiteral(sarg2, literal.getType(),
                   literal.getTypeName());
-          // Now we've strengthened containsNull to false, try to simplify again
+          // Now we've strengthened the Sarg, try to simplify again
           return simplifySearch(
               call.clone(call.type, ImmutableList.of(a, literal2)),
               unknownAs);
@@ -2697,22 +2702,13 @@
       final RexSargBuilder b =
           map.computeIfAbsent(e, e2 ->
               addFluent(newTerms, new RexSargBuilder(e2, rexBuilder, negate)));
-      switch (kind) {
+      switch (negate ? kind.negate() : kind) {
       case IS_NULL:
-        if (negate) {
-          ++b.nullTermCount;
-          b.addAll();
-        } else {
-          ++b.nullTermCount;
-        }
+        b.nullAs = b.nullAs.or(TRUE);
         return true;
       case IS_NOT_NULL:
-        if (negate) {
-          ++b.notNullTermCount;
-        } else {
-          ++b.notNullTermCount;
-          b.addAll();
-        }
+        b.nullAs = b.nullAs.or(FALSE);
+        b.addAll();
         return true;
       default:
         throw new AssertionError("unexpected " + kind);
@@ -2764,35 +2760,8 @@
       }
     }
 
-    /**
-     * Returns whether the merged {@code sarg} with given {@code unknownAs} keeps the semantics,
-     * The merge can not go ahead if the semantics change.
-     *
-     * @return true if the semantics does not change
-     */
-    private static boolean canMerge(Sarg sarg, RexUnknownAs unknownAs) {
-      final boolean isAllOrNone = sarg.isAll() || sarg.isNone();
-      final boolean containsNull = sarg.containsNull;
-      switch (unknownAs) {
-      case UNKNOWN:
-        // "unknown as unknown" can not be simplified to
-        // "IS NULL"/"IS NOT NULL"/"TRUE"/"FALSE"
-        return !isAllOrNone;
-      case TRUE:
-        // "unknown as true" can not be simplified to
-        // "false" or "IS NOT NULL"
-        return containsNull || !isAllOrNone;
-      case FALSE:
-        // "unknown as false" can not be simplified to
-        // "true" or "IS NULL"
-        return !containsNull || !isAllOrNone;
-      default:
-        return true;
-      }
-    }
-
     /** Returns whether it is worth to fix and convert to {@code SEARCH} calls. */
-    boolean needToFix(RexUnknownAs unknownAs) {
+    boolean needToFix() {
       // Fix and converts to SEARCH if:
       // 1. A Sarg has complexity greater than 1;
       // 2. The terms are reduced as simpler Sarg points;
@@ -2802,15 +2771,9 @@
       // method. "build().complexity()" would be a better estimate, if we could
       // switch to it breaking lots of plans.
       final Collection<RexSargBuilder> builders = map.values();
-      if (builders.stream().anyMatch(b -> b.build(false).complexity() > 1)) {
-        return true;
-      }
-      if (builders.size() == 1
-          && !canMerge(builders.iterator().next().build(), unknownAs)) {
-        return false;
-      }
-      return newTermsCount == 1
-          && map.values().stream().allMatch(b -> simpleSarg(b.build()));
+      return builders.stream().anyMatch(b -> b.build(false).complexity() > 1)
+          || newTermsCount == 1
+          && builders.stream().allMatch(b -> simpleSarg(b.build()));
     }
 
     /**
@@ -2823,14 +2786,16 @@
 
     /** If a term is a call to {@code SEARCH} on a {@link RexSargBuilder},
      * converts it to a {@code SEARCH} on a {@link Sarg}. */
-    static RexNode fix(RexBuilder rexBuilder, RexNode term) {
+    static RexNode fix(RexBuilder rexBuilder, RexNode term,
+        RexUnknownAs unknownAs) {
       if (term instanceof RexSargBuilder) {
         final RexSargBuilder sargBuilder = (RexSargBuilder) term;
         final Sarg sarg = sargBuilder.build();
         if (sarg.complexity() <= 1 && simpleSarg(sarg)) {
           // Expand small sargs into comparisons in order to avoid plan changes
           // and better readability.
-          return RexUtil.sargRef(rexBuilder, sargBuilder.ref, sarg, term.getType());
+          return RexUtil.sargRef(rexBuilder, sargBuilder.ref, sarg,
+              term.getType(), unknownAs);
         }
         return rexBuilder.makeCall(SqlStdOperatorTable.SEARCH, sargBuilder.ref,
             rexBuilder.makeSearchArgumentLiteral(sarg, term.getType()));
@@ -2844,7 +2809,22 @@
    * traverses a list of OR or AND terms.
    *
    * <p>The {@link SargCollector#fix} method converts it to an immutable
-   * literal. */
+   * literal.
+   *
+   * <p>The {@link #nullAs} field will become {@link Sarg#nullAs}, as follows:
+   *
+   * <ul>
+   * <li>If there is at least one term that returns TRUE when the argument
+   * is NULL, then the overall value will be TRUE; failing that,
+   * <li>if there is at least one term that returns UNKNOWN when the argument
+   * is NULL, then the overall value will be UNKNOWN; failing that,
+   * <li>the value will be FALSE.
+   * </ul>
+   *
+   * <p>This is analogous to the behavior of OR in three-valued logic:
+   * {@code TRUE OR UNKNOWN OR FALSE} returns {@code TRUE};
+   * {@code UNKNOWN OR FALSE OR UNKNOWN} returns {@code UNKNOWN};
+   * {@code FALSE OR FALSE} returns {@code FALSE}. */
   @SuppressWarnings("BetaApi")
   private static class RexSargBuilder extends RexNode {
     final RexNode ref;
@@ -2852,8 +2832,7 @@
     final boolean negate;
     final List<RelDataType> types = new ArrayList<>();
     final RangeSet<Comparable> rangeSet = TreeRangeSet.create();
-    int notNullTermCount;
-    int nullTermCount;
+    RexUnknownAs nullAs = FALSE;
 
     RexSargBuilder(RexNode ref, RexBuilder rexBuilder, boolean negate) {
       this.ref = requireNonNull(ref, "ref");
@@ -2863,8 +2842,7 @@
 
     @Override public String toString() {
       return "SEARCH(" + ref + ", " + (negate ? "NOT " : "") + rangeSet
-          + ", " + (nullTermCount + notNullTermCount)
-          + " terms of which " + nullTermCount + " allow null)";
+          + "; NULL AS " + nullAs + ")";
     }
 
     <C extends Comparable<C>> Sarg<C> build() {
@@ -2873,11 +2851,11 @@
 
     @SuppressWarnings({"rawtypes", "unchecked", "UnstableApiUsage"})
     <C extends Comparable<C>> Sarg<C> build(boolean negate) {
+      final RangeSet<C> r = (RangeSet) this.rangeSet;
       if (negate) {
-        return Sarg.of(notNullTermCount == 0,
-            (RangeSet) rangeSet.complement());
+        return Sarg.of(nullAs.negate(), r.complement());
       } else {
-        return Sarg.of(nullTermCount > 0, (RangeSet) rangeSet);
+        return Sarg.of(nullAs, r);
       }
     }
 
@@ -2915,16 +2893,32 @@
     void addRange(Range<Comparable> range, RelDataType type) {
       types.add(type);
       rangeSet.add(range);
-      ++notNullTermCount;
+      nullAs = nullAs.or(UNKNOWN);
     }
 
+    @SuppressWarnings({"UnstableApiUsage", "rawtypes", "unchecked"})
     void addSarg(Sarg sarg, boolean negate, RelDataType type) {
-      types.add(type);
-      rangeSet.addAll(negate ? sarg.rangeSet.complement() : sarg.rangeSet);
-      if (sarg.containsNull) {
-        ++nullTermCount;
+      final RangeSet r;
+      final RexUnknownAs nullAs;
+      if (negate) {
+        r = sarg.rangeSet.complement();
+        nullAs = sarg.nullAs.negate();
       } else {
-        ++notNullTermCount;
+        r = sarg.rangeSet;
+        nullAs = sarg.nullAs;
+      }
+      types.add(type);
+      rangeSet.addAll(r);
+      switch (nullAs) {
+      case TRUE:
+        this.nullAs = this.nullAs.or(TRUE);
+        break;
+      case FALSE:
+        this.nullAs = this.nullAs.or(FALSE);
+        break;
+      case UNKNOWN:
+        this.nullAs = this.nullAs.or(UNKNOWN);
+        break;
       }
     }
   }
diff --git a/core/src/main/java/org/apache/calcite/rex/RexUnknownAs.java b/core/src/main/java/org/apache/calcite/rex/RexUnknownAs.java
index 99841c3..6ec514e 100644
--- a/core/src/main/java/org/apache/calcite/rex/RexUnknownAs.java
+++ b/core/src/main/java/org/apache/calcite/rex/RexUnknownAs.java
@@ -103,4 +103,21 @@
       return UNKNOWN;
     }
   }
+
+  /** Combines this with another {@code RexUnknownAs} in the same way as the
+   * three-valued logic of OR.
+   *
+   * <p>For example, {@code TRUE or FALSE} returns {@code TRUE};
+   * {@code FALSE or UNKNOWN} returns {@code UNKNOWN}. */
+  public RexUnknownAs or(RexUnknownAs other) {
+    switch (this) {
+    case TRUE:
+      return this;
+    case UNKNOWN:
+      return other == TRUE ? other : this;
+    case FALSE:
+    default:
+      return other;
+    }
+  }
 }
diff --git a/core/src/main/java/org/apache/calcite/rex/RexUtil.java b/core/src/main/java/org/apache/calcite/rex/RexUtil.java
index d02da01..54899dc 100644
--- a/core/src/main/java/org/apache/calcite/rex/RexUtil.java
+++ b/core/src/main/java/org/apache/calcite/rex/RexUtil.java
@@ -597,17 +597,14 @@
   }
 
   @SuppressWarnings("BetaApi")
-  public static <C extends Comparable<C>> RexNode sargRef(
-      RexBuilder rexBuilder, RexNode ref, Sarg<C> sarg, RelDataType type) {
-    if (sarg.isAll()) {
-      if (sarg.containsNull) {
-        return rexBuilder.makeLiteral(true);
-      } else {
-        return rexBuilder.makeCall(SqlStdOperatorTable.IS_NOT_NULL, ref);
-      }
+  public static <C extends Comparable<C>> RexNode sargRef(RexBuilder rexBuilder,
+      RexNode ref, Sarg<C> sarg, RelDataType type, RexUnknownAs unknownAs) {
+    if (sarg.isAll() || sarg.isNone()) {
+      return simpleSarg(rexBuilder, ref, sarg, unknownAs);
     }
     final List<RexNode> orList = new ArrayList<>();
-    if (sarg.containsNull) {
+    if (sarg.nullAs == RexUnknownAs.TRUE
+        && unknownAs == RexUnknownAs.UNKNOWN) {
       orList.add(rexBuilder.makeCall(SqlStdOperatorTable.IS_NULL, ref));
     }
     if (sarg.isPoints()) {
@@ -631,7 +628,50 @@
           new RangeToRex<>(ref, orList, rexBuilder, type);
       RangeSets.forEach(sarg.rangeSet, consumer);
     }
-    return composeDisjunction(rexBuilder, orList);
+    RexNode node = composeDisjunction(rexBuilder, orList);
+    if (sarg.nullAs == RexUnknownAs.FALSE
+        && unknownAs == RexUnknownAs.UNKNOWN) {
+      node =
+          rexBuilder.makeCall(SqlStdOperatorTable.AND,
+              rexBuilder.makeCall(SqlStdOperatorTable.IS_NOT_NULL, ref),
+              node);
+    }
+    return node;
+  }
+
+  /** Expands an 'all' or 'none' sarg. */
+  public static <C extends Comparable<C>> RexNode simpleSarg(RexBuilder rexBuilder,
+      RexNode ref, Sarg<C> sarg, RexUnknownAs unknownAs) {
+    assert sarg.isAll() || sarg.isNone();
+    final RexUnknownAs nullAs =
+        sarg.nullAs == RexUnknownAs.UNKNOWN ? unknownAs
+            : sarg.nullAs;
+    if (sarg.isAll()) {
+      switch (nullAs) {
+      case TRUE:
+        return rexBuilder.makeLiteral(true);
+      case FALSE:
+        return rexBuilder.makeCall(SqlStdOperatorTable.IS_NOT_NULL, ref);
+      case UNKNOWN:
+        // "x IS NOT NULL OR UNKNOWN"
+        return rexBuilder.makeCall(SqlStdOperatorTable.OR,
+            rexBuilder.makeCall(SqlStdOperatorTable.IS_NOT_NULL, ref),
+            rexBuilder.makeNullLiteral(
+                rexBuilder.typeFactory.createSqlType(SqlTypeName.BOOLEAN)));
+      }
+    }
+    if (sarg.isNone()) {
+      switch (nullAs) {
+      case TRUE:
+        return rexBuilder.makeCall(SqlStdOperatorTable.IS_NULL, ref);
+      case FALSE:
+        return rexBuilder.makeLiteral(false);
+      case UNKNOWN:
+        // "CASE WHEN x IS NULL THEN UNKNOWN ELSE FALSE END", or "x <> x"
+        return rexBuilder.makeCall(SqlStdOperatorTable.NOT_EQUALS, ref, ref);
+      }
+    }
+    throw new AssertionError();
   }
 
   private static RexNode deref(@Nullable RexProgram program, RexNode node) {
@@ -3054,7 +3094,8 @@
             (RexLiteral) deref(program, call.operands.get(1));
         final Sarg sarg = requireNonNull(literal.getValueAs(Sarg.class), "Sarg");
         if (maxComplexity < 0 || sarg.complexity() < maxComplexity) {
-          return sargRef(rexBuilder, ref, sarg, literal.getType());
+          return sargRef(rexBuilder, ref, sarg, literal.getType(),
+              RexUnknownAs.UNKNOWN);
         }
         // Sarg is complex (therefore useful); fall through
       default:
diff --git a/core/src/main/java/org/apache/calcite/sql/fun/SqlSearchOperator.java b/core/src/main/java/org/apache/calcite/sql/fun/SqlSearchOperator.java
index dc20e72..8fcd9ed 100644
--- a/core/src/main/java/org/apache/calcite/sql/fun/SqlSearchOperator.java
+++ b/core/src/main/java/org/apache/calcite/sql/fun/SqlSearchOperator.java
@@ -17,6 +17,7 @@
 package org.apache.calcite.sql.fun;
 
 import org.apache.calcite.rel.type.RelDataType;
+import org.apache.calcite.rex.RexUnknownAs;
 import org.apache.calcite.sql.SqlInternalOperator;
 import org.apache.calcite.sql.SqlKind;
 import org.apache.calcite.sql.SqlOperatorBinding;
@@ -44,16 +45,17 @@
    * It is evident from the expansion, "x = 10", but holds for all Sarg
    * values.
    *
-   * <p>If {@link Sarg#containsNull} is true, SEARCH will never return
-   * UNKNOWN. For example, {@code SEARCH(x, Sarg[10 OR NULL])} expands to
-   * {@code x = 10 OR x IS NOT NULL}, which returns {@code TRUE} if
+   * <p>If {@link Sarg#nullAs} is TRUE or FALSE, SEARCH will never return
+   * UNKNOWN. For example, {@code SEARCH(x, Sarg[10; NULL AS UNKNOWN])} expands
+   * to {@code x = 10 OR x IS NOT NULL}, which returns {@code TRUE} if
    * {@code x} is NULL, {@code TRUE} if {@code x} is 10, and {@code FALSE}
    * for all other values.
    */
   private static RelDataType makeNullable(SqlOperatorBinding binding,
       RelDataType type) {
     final boolean nullable = binding.getOperandType(0).isNullable()
-        && !getOperandLiteralValueOrThrow(binding, 1, Sarg.class).containsNull;
+        && getOperandLiteralValueOrThrow(binding, 1, Sarg.class).nullAs
+        == RexUnknownAs.UNKNOWN;
     return binding.getTypeFactory().createTypeWithNullability(type, nullable);
   }
 }
diff --git a/core/src/main/java/org/apache/calcite/util/Sarg.java b/core/src/main/java/org/apache/calcite/util/Sarg.java
index 55ed4d0..ef3822c 100644
--- a/core/src/main/java/org/apache/calcite/util/Sarg.java
+++ b/core/src/main/java/org/apache/calcite/util/Sarg.java
@@ -17,6 +17,7 @@
 package org.apache.calcite.util;
 
 import org.apache.calcite.linq4j.Ord;
+import org.apache.calcite.rex.RexUnknownAs;
 import org.apache.calcite.sql.fun.SqlStdOperatorTable;
 
 import com.google.common.collect.ImmutableRangeSet;
@@ -63,36 +64,111 @@
  *
  * @see SqlStdOperatorTable#SEARCH
  */
-@SuppressWarnings({"BetaApi", "type.argument.type.incompatible"})
+@SuppressWarnings({"BetaApi", "type.argument.type.incompatible", "UnstableApiUsage"})
 public class Sarg<C extends Comparable<C>> implements Comparable<Sarg<C>> {
   public final RangeSet<C> rangeSet;
+  public final RexUnknownAs nullAs;
+  @Deprecated // to be removed before 1.28
   public final boolean containsNull;
   public final int pointCount;
 
-  private Sarg(ImmutableRangeSet<C> rangeSet, boolean containsNull) {
+  /** Returns FALSE for all null and not-null values.
+   *
+   * <p>{@code SEARCH(x, FALSE)} is equivalent to {@code FALSE}. */
+  private static final SpecialSarg FALSE =
+      new SpecialSarg(ImmutableRangeSet.of(), RexUnknownAs.FALSE,
+          "Sarg[FALSE]", 2);
+
+  /** Returns TRUE for all not-null values, FALSE for null.
+   *
+   * <p>{@code SEARCH(x, IS_NOT_NULL)} is equivalent to
+   * {@code x IS NOT NULL}. */
+  private static final SpecialSarg IS_NOT_NULL =
+      new SpecialSarg(ImmutableRangeSet.of().complement(), RexUnknownAs.FALSE,
+          "Sarg[IS NOT NULL]", 3);
+
+  /** Returns FALSE for all not-null values, TRUE for null.
+   *
+   * <p>{@code SEARCH(x, IS_NULL)} is equivalent to {@code x IS NULL}. */
+  private static final SpecialSarg IS_NULL =
+      new SpecialSarg(ImmutableRangeSet.of(), RexUnknownAs.TRUE,
+          "Sarg[IS NULL]", 4);
+
+  /** Returns TRUE for all null and not-null values.
+   *
+   * <p>{@code SEARCH(x, TRUE)} is equivalent to {@code TRUE}. */
+  private static final SpecialSarg TRUE =
+      new SpecialSarg(ImmutableRangeSet.of().complement(), RexUnknownAs.TRUE,
+          "Sarg[TRUE]", 5);
+
+  /** Returns FALSE for all not-null values, UNKNOWN for null.
+   *
+   * <p>{@code SEARCH(x, NOT_EQUAL)} is equivalent to {@code x <> x}. */
+  private static final SpecialSarg NOT_EQUAL =
+      new SpecialSarg(ImmutableRangeSet.of(), RexUnknownAs.UNKNOWN,
+          "Sarg[<>]", 6);
+
+  /** Returns TRUE for all not-null values, UNKNOWN for null.
+   *
+   * <p>{@code SEARCH(x, EQUAL)} is equivalent to {@code x = x}. */
+  private static final SpecialSarg EQUAL =
+      new SpecialSarg(ImmutableRangeSet.of().complement(), RexUnknownAs.UNKNOWN,
+          "Sarg[=]", 7);
+
+  private Sarg(ImmutableRangeSet<C> rangeSet, RexUnknownAs nullAs) {
     this.rangeSet = Objects.requireNonNull(rangeSet, "rangeSet");
-    this.containsNull = containsNull;
+    this.nullAs = Objects.requireNonNull(nullAs, "nullAs");
+    this.containsNull = nullAs == RexUnknownAs.TRUE;
     this.pointCount = RangeSets.countPoints(rangeSet);
   }
 
-  /** Creates a search argument. */
+  @Deprecated // to be removed before 2.0
   public static <C extends Comparable<C>> Sarg<C> of(boolean containsNull,
       RangeSet<C> rangeSet) {
-    return new Sarg<>(ImmutableRangeSet.copyOf(rangeSet), containsNull);
+    return of(containsNull ? RexUnknownAs.TRUE : RexUnknownAs.UNKNOWN,
+        rangeSet);
+  }
+
+  /** Creates a search argument. */
+  public static <C extends Comparable<C>> Sarg<C> of(RexUnknownAs nullAs,
+      RangeSet<C> rangeSet) {
+    if (rangeSet.isEmpty()) {
+      switch (nullAs) {
+      case FALSE:
+        return FALSE;
+      case TRUE:
+        return IS_NULL;
+      default:
+        return NOT_EQUAL;
+      }
+    }
+    if (rangeSet.equals(RangeSets.rangeSetAll())) {
+      switch (nullAs) {
+      case FALSE:
+        return IS_NOT_NULL;
+      case TRUE:
+        return TRUE;
+      default:
+        return EQUAL;
+      }
+    }
+    return new Sarg<>(ImmutableRangeSet.copyOf(rangeSet), nullAs);
   }
 
   /**
    * {@inheritDoc}
    *
-   * <p>Produces a similar result to {@link RangeSet}, but adds ", null"
-   * if nulls are matched, and simplifies point ranges. For example,
-   * the Sarg that allows the range set
+   * <p>Produces a similar result to {@link RangeSet},
+   * but adds "; NULL AS FALSE" or "; NULL AS TRUE" to indicate {@link #nullAs},
+   * and simplifies point ranges.
+   *
+   * <p>For example, the Sarg that allows the range set
    *
    * <blockquote>{@code [[7..7], [9..9], (10..+∞)]}</blockquote>
    *
-   * and also null is printed as
+   * <p>and also null is printed as
    *
-   * <blockquote>{@code Sarg[7, 9, (10..+∞) OR NULL]}</blockquote>
+   * <blockquote>{@code Sarg[7, 9, (10..+∞); NULL AS TRUE]}</blockquote>
    */
   @Override public String toString() {
     final StringBuilder sb = new StringBuilder();
@@ -104,12 +180,6 @@
    * with each embedded value. */
   public StringBuilder printTo(StringBuilder sb,
       BiConsumer<StringBuilder, C> valuePrinter) {
-    if (isAll()) {
-      return sb.append(containsNull ? "Sarg[TRUE]" : "Sarg[NOT NULL]");
-    }
-    if (isNone()) {
-      return sb.append(containsNull ? "Sarg[NULL]" : "Sarg[FALSE]");
-    }
     sb.append("Sarg[");
     final RangeSets.Consumer<C> printer = RangeSets.printer(sb, valuePrinter);
     Ord.forEach(rangeSet.asRanges(), (r, i) -> {
@@ -118,10 +188,16 @@
       }
       RangeSets.forEach(r, printer);
     });
-    if (containsNull) {
-      sb.append(" OR NULL");
+    switch (nullAs) {
+    case FALSE:
+      return sb.append("; NULL AS FALSE]");
+    case TRUE:
+      return sb.append("; NULL AS TRUE]");
+    case UNKNOWN:
+      return sb.append("]");
+    default:
+      throw new AssertionError();
     }
-    return sb.append("]");
   }
 
   @Override public int compareTo(Sarg<C> o) {
@@ -129,26 +205,26 @@
   }
 
   @Override public int hashCode() {
-    return RangeSets.hashCode(rangeSet) * 31 + (containsNull ? 2 : 3);
+    return RangeSets.hashCode(rangeSet) * 31 + nullAs.ordinal();
   }
 
   @Override public boolean equals(@Nullable Object o) {
     return o == this
         || o instanceof Sarg
-        && containsNull == ((Sarg) o).containsNull
+        && nullAs == ((Sarg) o).nullAs
         && rangeSet.equals(((Sarg) o).rangeSet);
   }
 
   /** Returns whether this Sarg includes all values (including or not including
    * null). */
   public boolean isAll() {
-    return rangeSet.equals(RangeSets.rangeSetAll());
+    return false;
   }
 
   /** Returns whether this Sarg includes no values (including or not including
    * null). */
   public boolean isNone() {
-    return rangeSet.isEmpty();
+    return false;
   }
 
   /** Returns whether this Sarg is a collection of 1 or more points (and perhaps
@@ -198,7 +274,7 @@
     } else {
       complexity = rangeSet.asRanges().size();
     }
-    if (containsNull) {
+    if (nullAs == RexUnknownAs.TRUE) {
       ++complexity;
     }
     return complexity;
@@ -206,6 +282,61 @@
 
   /** Returns a Sarg that matches a value if and only this Sarg does not. */
   public Sarg negate() {
-    return Sarg.of(!containsNull, rangeSet.complement());
+    return Sarg.of(nullAs.negate(), rangeSet.complement());
+  }
+
+  /** Sarg whose range is all or none.
+   *
+   * <p>There are only 6 instances: {all, none} * {true, false, unknown}.
+   *
+   * @param <C> Value type */
+  private static class SpecialSarg<C extends Comparable<C>> extends Sarg<C> {
+    final String name;
+    final int ordinal;
+
+    SpecialSarg(ImmutableRangeSet<C> rangeSet, RexUnknownAs nullAs, String name,
+        int ordinal) {
+      super(rangeSet, nullAs);
+      this.name = name;
+      this.ordinal = ordinal;
+      assert rangeSet.isEmpty() == ((ordinal & 1) == 0);
+      assert rangeSet.equals(RangeSets.rangeSetAll()) == ((ordinal & 1) == 1);
+    }
+
+    @Override public boolean equals(@Nullable Object o) {
+      return this == o;
+    }
+
+    @Override public int hashCode() {
+      return ordinal;
+    }
+
+    @Override public boolean isAll() {
+      return (ordinal & 1) == 1;
+    }
+
+    @Override public boolean isNone() {
+      return (ordinal & 1) == 0;
+    }
+
+    @Override public int complexity() {
+      switch (ordinal) {
+      case 2: // Sarg[FALSE]
+        return 0; // for backwards compatibility
+      case 5: // Sarg[TRUE]
+        return 2; // for backwards compatibility
+      default:
+        return 1;
+      }
+    }
+
+    @Override public StringBuilder printTo(StringBuilder sb,
+        BiConsumer<StringBuilder, C> valuePrinter) {
+      return sb.append(name);
+    }
+
+    @Override public String toString() {
+      return name;
+    }
   }
 }
diff --git a/core/src/test/java/org/apache/calcite/rex/RexProgramBuilderBase.java b/core/src/test/java/org/apache/calcite/rex/RexProgramBuilderBase.java
index f1a48a0..6b25f2c 100644
--- a/core/src/test/java/org/apache/calcite/rex/RexProgramBuilderBase.java
+++ b/core/src/test/java/org/apache/calcite/rex/RexProgramBuilderBase.java
@@ -187,18 +187,22 @@
   }
 
   protected RexNode isFalse(RexNode node) {
+    assert node.getType().getSqlTypeName() == SqlTypeName.BOOLEAN;
     return rexBuilder.makeCall(SqlStdOperatorTable.IS_FALSE, node);
   }
 
   protected RexNode isNotFalse(RexNode node) {
+    assert node.getType().getSqlTypeName() == SqlTypeName.BOOLEAN;
     return rexBuilder.makeCall(SqlStdOperatorTable.IS_NOT_FALSE, node);
   }
 
   protected RexNode isTrue(RexNode node) {
+    assert node.getType().getSqlTypeName() == SqlTypeName.BOOLEAN;
     return rexBuilder.makeCall(SqlStdOperatorTable.IS_TRUE, node);
   }
 
   protected RexNode isNotTrue(RexNode node) {
+    assert node.getType().getSqlTypeName() == SqlTypeName.BOOLEAN;
     return rexBuilder.makeCall(SqlStdOperatorTable.IS_NOT_TRUE, node);
   }
 
diff --git a/core/src/test/java/org/apache/calcite/rex/RexProgramTest.java b/core/src/test/java/org/apache/calcite/rex/RexProgramTest.java
index 4cd61fe..1a8a748 100644
--- a/core/src/test/java/org/apache/calcite/rex/RexProgramTest.java
+++ b/core/src/test/java/org/apache/calcite/rex/RexProgramTest.java
@@ -931,15 +931,17 @@
     checkSimplify(rexBuilder.makeCall(SqlStdOperatorTable.IS_NOT_NULL, aRef),
         "true");
 
-    // condition, and the inverse - nothing to do due to null values
-    checkSimplify2(and(le(hRef, literal(1)), gt(hRef, literal(1))),
-        "AND(<=(?0.h, 1), >(?0.h, 1))",
+    // condition, and the inverse
+    checkSimplify3(and(le(hRef, literal(1)), gt(hRef, literal(1))),
+        "<>(?0.h, ?0.h)",
+        "false",
         "false");
 
     checkSimplify(and(le(hRef, literal(1)), ge(hRef, literal(1))), "=(?0.h, 1)");
 
-    checkSimplify2(and(lt(hRef, literal(1)), eq(hRef, literal(1)), ge(hRef, literal(1))),
-        "AND(<(?0.h, 1), =(?0.h, 1), >=(?0.h, 1))",
+    checkSimplify3(and(lt(hRef, literal(1)), eq(hRef, literal(1)), ge(hRef, literal(1))),
+        "<>(?0.h, ?0.h)",
+        "false",
         "false");
 
     checkSimplify(and(lt(hRef, literal(1)), or(falseLiteral, falseLiteral)),
@@ -1404,16 +1406,19 @@
 
   @Test void testSimplifyOrTerms() {
     final RelDataType intType = typeFactory.createSqlType(SqlTypeName.INTEGER);
+    final RelDataType boolType = typeFactory.createSqlType(SqlTypeName.BOOLEAN);
     final RelDataType rowType = typeFactory.builder()
         .add("a", intType).nullable(false)
         .add("b", intType).nullable(true)
         .add("c", intType).nullable(true)
+        .add("d", boolType).nullable(true)
         .build();
 
     final RexDynamicParam range = rexBuilder.makeDynamicParam(rowType, 0);
     final RexNode aRef = rexBuilder.makeFieldAccess(range, 0);
     final RexNode bRef = rexBuilder.makeFieldAccess(range, 1);
     final RexNode cRef = rexBuilder.makeFieldAccess(range, 2);
+    final RexNode dRef = rexBuilder.makeFieldAccess(range, 3);
     final RexLiteral literal1 = rexBuilder.makeExactLiteral(BigDecimal.ONE);
     final RexLiteral literal2 = rexBuilder.makeExactLiteral(BigDecimal.valueOf(2));
     final RexLiteral literal3 = rexBuilder.makeExactLiteral(BigDecimal.valueOf(3));
@@ -1495,11 +1500,11 @@
     // Careful of the excluded middle!
     // We cannot simplify "b <> 1 or b = 1" to "true" because if b is null, the
     // result is unknown.
-    // TODO: "b is not unknown" would be the best simplification.
+    // TODO: "b = b" would be the best simplification.
     final RexNode simplified =
         this.simplify.simplifyUnknownAs(neOrEq, RexUnknownAs.UNKNOWN);
     assertThat(simplified.toString(),
-        equalTo("OR(<>(?0.b, 1), =(?0.b, 1))"));
+        equalTo("OR(IS NOT NULL(?0.b), null)"));
 
     // "a is null or a is not null" ==> "true"
     checkSimplifyFilter(
@@ -1554,12 +1559,12 @@
             isNull(cRef)),
         "OR(IS NULL(?0.c), IS NOT NULL(?0.b))");
 
-    // "b is null or b is not false" => "b is null or b"
-    // (because after the first term we know that b cannot be null)
+    // "d is null or d is not false" => "d is null or d"
+    // (because after the first term we know that d cannot be null)
     checkSimplifyFilter(
-        or(isNull(bRef),
-            isNotFalse(bRef)),
-        "OR(IS NULL(?0.b), ?0.b)");
+        or(isNull(dRef),
+            isNotFalse(dRef)),
+        "OR(IS NULL(?0.d), ?0.d)");
 
     // multiple predicates are handled correctly
     checkSimplifyFilter(
@@ -1603,7 +1608,7 @@
     // a is null or a >= 15
     RexNode expr = or(isNull(aRef),
         ge(aRef, literal(15)));
-    checkSimplify(expr, "SEARCH($0, Sarg[[15..+\u221e) OR NULL])")
+    checkSimplify(expr, "SEARCH($0, Sarg[[15..+\u221e); NULL AS TRUE])")
         .expandedSearch("OR(IS NULL($0), >=($0, 15))");
   }
 
@@ -1622,7 +1627,7 @@
         ge(aRef, literal(15)));
     // [CALCITE-4190] causes "or a >= 15" to disappear from the simplified form.
     final String simplified =
-        "SEARCH($0, Sarg[(0..12), [15..+\u221e) OR NULL])";
+        "SEARCH($0, Sarg[(0..12), [15..+\u221e); NULL AS TRUE])";
     final String expanded =
         "OR(IS NULL($0), AND(>($0, 0), <($0, 12)), >=($0, 15))";
     checkSimplify(expr, simplified)
@@ -1651,7 +1656,7 @@
                 eq(aRef, literal(5)))),
         isNull(aRef));
     final String simplified =
-        "SEARCH($0, Sarg[(-\u221e..3), (3..5), (5..+\u221e) OR NULL])";
+        "SEARCH($0, Sarg[(-\u221e..3), (3..5), (5..+\u221e); NULL AS TRUE])";
     final String expanded = "OR(IS NULL($0), AND(<>($0, 3), <>($0, 5)))";
     checkSimplify(expr, simplified)
         .expandedSearch(expanded);
@@ -1679,8 +1684,8 @@
         isNotNull(aRef),
         gt(aRef, literal(3)),
         lt(aRef, literal(10)));
-    final String simplified = "SEARCH($0, Sarg[(3..10)])";
-    final String expanded = "AND(>($0, 3), <($0, 10))";
+    final String simplified = "SEARCH($0, Sarg[(3..10); NULL AS FALSE])";
+    final String expanded = "AND(IS NOT NULL($0), AND(>($0, 3), <($0, 10)))";
     checkSimplify(expr, simplified)
         .expandedSearch(expanded);
   }
@@ -1761,13 +1766,13 @@
   @Test void testSimplifyEqOrIsNullAndEq() {
     // (deptno = 20 OR deptno IS NULL) AND deptno = 10
     //   ==>
-    // false
+    // deptno <> deptno
     final RexNode e =
         and(
             or(eq(vInt(), literal(20)),
                 isNull(vInt())),
         eq(vInt(), literal(10)));
-    checkSimplify(e, "false");
+    checkSimplify3(e, "<>(?0.int0, ?0.int0)", "false", "IS NULL(?0.int0)");
   }
 
   @Test void testSimplifyEqOrIsNullAndEqSame() {
@@ -1795,12 +1800,13 @@
     // deptno in (20, 10) and deptno = 30
     //   ==>
     // false
-    checkSimplify2(
+    checkSimplify3(
         and(
         in(vInt(), literal(20), literal(10)),
         eq(vInt(), literal(30))),
-        "AND(SEARCH(?0.int0, Sarg[10, 20]), =(?0.int0, 30))",
-        "false");
+        "<>(?0.int0, ?0.int0)",
+        "false",
+        "IS NULL(?0.int0)");
   }
 
   @Test void testSimplifyInOr() {
@@ -1816,26 +1822,24 @@
 
   /** Test strategies for {@code SargCollector.canMerge(Sarg, RexUnknownAs)}. */
   @Test void testSargMerge() {
-    checkSimplify2(
-        or(
-            ne(vInt(), literal(1)),
+    checkSimplify3(
+        or(ne(vInt(), literal(1)),
             eq(vInt(), literal(1))),
-        "OR(<>(?0.int0, 1), =(?0.int0, 1))",
-        "IS NOT NULL(?0.int0)");
-    checkSimplify2(
-        and(
-            gt(vInt(), literal(5)),
+        "OR(IS NOT NULL(?0.int0), null)",
+        "IS NOT NULL(?0.int0)",
+        "true");
+    checkSimplify3(
+        and(gt(vInt(), literal(5)),
             lt(vInt(), literal(3))),
-        "AND(>(?0.int0, 5), <(?0.int0, 3))",
-        "false");
+        "<>(?0.int0, ?0.int0)",
+        "false",
+        "IS NULL(?0.int0)");
     checkSimplify(
-        or(
-            falseLiteral,
+        or(falseLiteral,
             isNull(vInt())),
         "IS NULL(?0.int0)");
     checkSimplify(
-        and(
-            trueLiteral,
+        and(trueLiteral,
             isNotNull(vInt())),
         "IS NOT NULL(?0.int0)");
   }
@@ -2779,12 +2783,11 @@
   }
 
   @Test void testSimplifyOrIsNull() {
+    String expected = "SEARCH(?0.int0, Sarg[10; NULL AS TRUE])";
     // x = 10 OR x IS NULL
-    checkSimplify(or(eq(vInt(0), literal(10)), isNull(vInt(0))),
-        "SEARCH(?0.int0, Sarg[10 OR NULL])");
+    checkSimplify(or(eq(vInt(0), literal(10)), isNull(vInt(0))), expected);
     // 10 = x OR x IS NULL
-    checkSimplify(or(eq(literal(10), vInt(0)), isNull(vInt(0))),
-        "SEARCH(?0.int0, Sarg[10 OR NULL])");
+    checkSimplify(or(eq(literal(10), vInt(0)), isNull(vInt(0))), expected);
   }
 
   @Test void testSimplifyOrNot() {
@@ -2819,49 +2822,52 @@
   @SuppressWarnings("UnstableApiUsage")
   @Test void testSargComplexity() {
     checkSarg("complexity of 'x is not null'",
-        Sarg.of(false, RangeSets.<Integer>rangeSetAll()),
-        is(1), is("Sarg[NOT NULL]"));
+        Sarg.of(RexUnknownAs.FALSE, RangeSets.<Integer>rangeSetAll()),
+        is(1), is("Sarg[IS NOT NULL]"));
     checkSarg("complexity of 'x is null'",
-        Sarg.of(true, ImmutableRangeSet.<Integer>of()),
-        is(1), is("Sarg[NULL]"));
+        Sarg.of(RexUnknownAs.TRUE, ImmutableRangeSet.<Integer>of()),
+        is(1), is("Sarg[IS NULL]"));
     checkSarg("complexity of 'false'",
-        Sarg.of(false, ImmutableRangeSet.<Integer>of()),
+        Sarg.of(RexUnknownAs.FALSE, ImmutableRangeSet.<Integer>of()),
         is(0), is("Sarg[FALSE]"));
     checkSarg("complexity of 'true'",
-        Sarg.of(true, RangeSets.<Integer>rangeSetAll()),
+        Sarg.of(RexUnknownAs.TRUE, RangeSets.<Integer>rangeSetAll()),
         is(2), is("Sarg[TRUE]"));
 
     checkSarg("complexity of 'x = 1'",
-        Sarg.of(false, ImmutableRangeSet.of(Range.singleton(1))),
+        Sarg.of(RexUnknownAs.UNKNOWN, ImmutableRangeSet.of(Range.singleton(1))),
         is(1), is("Sarg[1]"));
     checkSarg("complexity of 'x > 1'",
-        Sarg.of(false, ImmutableRangeSet.of(Range.greaterThan(1))),
+        Sarg.of(RexUnknownAs.UNKNOWN,
+            ImmutableRangeSet.of(Range.greaterThan(1))),
         is(1), is("Sarg[(1..+\u221E)]"));
     checkSarg("complexity of 'x >= 1'",
-        Sarg.of(false, ImmutableRangeSet.of(Range.atLeast(1))),
+        Sarg.of(RexUnknownAs.UNKNOWN, ImmutableRangeSet.of(Range.atLeast(1))),
         is(1), is("Sarg[[1..+\u221E)]"));
     checkSarg("complexity of 'x > 1 or x is null'",
-        Sarg.of(true, ImmutableRangeSet.of(Range.greaterThan(1))),
-        is(2), is("Sarg[(1..+\u221E) OR NULL]"));
+        Sarg.of(RexUnknownAs.TRUE, ImmutableRangeSet.of(Range.greaterThan(1))),
+        is(2), is("Sarg[(1..+\u221E); NULL AS TRUE]"));
     checkSarg("complexity of 'x <> 1'",
-        Sarg.of(false, ImmutableRangeSet.of(Range.singleton(1)).complement()),
+        Sarg.of(RexUnknownAs.UNKNOWN,
+            ImmutableRangeSet.of(Range.singleton(1)).complement()),
         is(1), is("Sarg[(-\u221E..1), (1..+\u221E)]"));
     checkSarg("complexity of 'x <> 1 or x is null'",
-        Sarg.of(true, ImmutableRangeSet.of(Range.singleton(1)).complement()),
-        is(2), is("Sarg[(-\u221E..1), (1..+\u221E) OR NULL]"));
+        Sarg.of(RexUnknownAs.TRUE,
+            ImmutableRangeSet.of(Range.singleton(1)).complement()),
+        is(2), is("Sarg[(-\u221E..1), (1..+\u221E); NULL AS TRUE]"));
     checkSarg("complexity of 'x < 10 or x >= 20'",
-        Sarg.of(false,
+        Sarg.of(RexUnknownAs.UNKNOWN,
             ImmutableRangeSet.copyOf(
                 ImmutableList.of(Range.lessThan(10), Range.atLeast(20)))),
         is(2), is("Sarg[(-\u221E..10), [20..+\u221E)]"));
     checkSarg("complexity of 'x in (2, 4, 6) or x > 20'",
-        Sarg.of(false,
+        Sarg.of(RexUnknownAs.UNKNOWN,
             ImmutableRangeSet.copyOf(
                 Arrays.asList(Range.singleton(2), Range.singleton(4),
                     Range.singleton(6), Range.greaterThan(20)))),
         is(4), is("Sarg[2, 4, 6, (20..+\u221E)]"));
     checkSarg("complexity of 'x between 3 and 8 or x between 10 and 20'",
-        Sarg.of(false,
+        Sarg.of(RexUnknownAs.UNKNOWN,
             ImmutableRangeSet.copyOf(
                 Arrays.asList(Range.closed(3, 8),
                     Range.closed(10, 20)))),
@@ -2886,7 +2892,7 @@
   }
 
   @Test void testIsNullRecursion() {
-    // make sure that simplifcation is visiting below isX expressions
+    // make sure that simplification is visiting below isX expressions
     checkSimplify(
         isNull(or(coalesce(nullBool, trueLiteral), falseLiteral)),
         "false");
@@ -3078,8 +3084,14 @@
    * RexSimplify should simplify more always true OR expressions</a>. */
   @Test void testSimplifyLike() {
     final RexNode ref = input(tVarchar(true, 10), 0);
-    checkSimplify(like(ref, literal("%")), "true");
-    checkSimplify(like(ref, literal("%"), literal("#")), "true");
+    checkSimplify(like(ref, literal("%")),
+        "OR(null, IS NOT NULL($0))");
+    checkSimplify(like(ref, literal("%"), literal("#")),
+        "OR(null, IS NOT NULL($0))");
+    checkSimplify(or(isNull(ref), like(ref, literal("%"))),
+        "true");
+    checkSimplify(or(isNull(ref), like(ref, literal("%"), literal("#"))),
+        "true");
     checkSimplifyUnchanged(like(ref, literal("%A")));
     checkSimplifyUnchanged(like(ref, literal("%A"), literal("#")));
   }
diff --git a/core/src/test/java/org/apache/calcite/rex/RexProgramTestBase.java b/core/src/test/java/org/apache/calcite/rex/RexProgramTestBase.java
index 0edf0c3..36820db 100644
--- a/core/src/test/java/org/apache/calcite/rex/RexProgramTestBase.java
+++ b/core/src/test/java/org/apache/calcite/rex/RexProgramTestBase.java
@@ -109,7 +109,7 @@
    *     as false
    */
   protected void checkSimplify2(RexNode node, String expected,
-                              String expectedFalse) {
+      String expectedFalse) {
     checkSimplify3_(node, expected, expectedFalse, expected);
     if (expected.equals(expectedFalse)) {
       throw new AssertionError("expected == expectedFalse; use checkSimplify");
@@ -117,7 +117,7 @@
   }
 
   protected void checkSimplify3(RexNode node, String expected,
-                              String expectedFalse, String expectedTrue) {
+      String expectedFalse, String expectedTrue) {
     checkSimplify3_(node, expected, expectedFalse, expectedTrue);
     if (expected.equals(expectedFalse) && expected.equals(expectedTrue)) {
       throw new AssertionError("expected == expectedFalse == expectedTrue; "
@@ -131,18 +131,10 @@
   protected SimplifiedNode checkSimplify3_(RexNode node, String expected,
       String expectedFalse, String expectedTrue) {
     final RexNode simplified =
-        simplify.simplifyUnknownAs(node, RexUnknownAs.UNKNOWN);
-    assertThat("simplify(unknown as unknown): " + node,
-        simplified.toString(), equalTo(expected));
+        checkSimplifyAs(node, RexUnknownAs.UNKNOWN, is(expected));
     if (node.getType().getSqlTypeName() == SqlTypeName.BOOLEAN) {
-      final RexNode simplified2 =
-          simplify.simplifyUnknownAs(node, RexUnknownAs.FALSE);
-      assertThat("simplify(unknown as false): " + node,
-          simplified2.toString(), equalTo(expectedFalse));
-      final RexNode simplified3 =
-          simplify.simplifyUnknownAs(node, RexUnknownAs.TRUE);
-      assertThat("simplify(unknown as true): " + node,
-          simplified3.toString(), equalTo(expectedTrue));
+      checkSimplifyAs(node, RexUnknownAs.FALSE, is(expectedFalse));
+      checkSimplifyAs(node, RexUnknownAs.TRUE, is(expectedTrue));
     } else {
       assertThat("node type is not BOOLEAN, so <<expectedFalse>> should match <<expected>>",
           expectedFalse, is(expected));
@@ -152,15 +144,21 @@
     return new SimplifiedNode(rexBuilder, node, simplified);
   }
 
-  protected Node checkSimplifyFilter(RexNode node, String expected) {
+  private RexNode checkSimplifyAs(RexNode node, RexUnknownAs unknownAs,
+      Matcher<String> matcher) {
     final RexNode simplified =
-        this.simplify.simplifyUnknownAs(node, RexUnknownAs.FALSE);
-    assertThat(simplified.toString(), equalTo(expected));
-    return node(node);
+        simplify.simplifyUnknownAs(node, unknownAs);
+    assertThat(("simplify(unknown as " + unknownAs + "): ") + node,
+        simplified.toString(), matcher);
+    return simplified;
   }
 
-  protected void checkSimplifyFilter(RexNode node, RelOptPredicateList predicates,
-                                   String expected) {
+  protected void checkSimplifyFilter(RexNode node, String expected) {
+    checkSimplifyAs(node, RexUnknownAs.FALSE, is(expected));
+  }
+
+  protected void checkSimplifyFilter(RexNode node,
+      RelOptPredicateList predicates, String expected) {
     final RexNode simplified =
         simplify.withPredicates(predicates)
             .simplifyUnknownAs(node, RexUnknownAs.FALSE);
diff --git a/core/src/test/java/org/apache/calcite/test/RelBuilderTest.java b/core/src/test/java/org/apache/calcite/test/RelBuilderTest.java
index c822ca7..b083332 100644
--- a/core/src/test/java/org/apache/calcite/test/RelBuilderTest.java
+++ b/core/src/test/java/org/apache/calcite/test/RelBuilderTest.java
@@ -683,7 +683,9 @@
                 builder.or(
                     builder.equals(builder.field("DEPTNO"),
                         builder.literal(20)),
-                    builder.and(builder.literal(null),
+                    builder.and(
+                        builder.cast(builder.literal(null),
+                            SqlTypeName.BOOLEAN),
                         builder.equals(builder.field("DEPTNO"),
                             builder.literal(10)),
                         builder.and(builder.isNull(builder.field(6)),
@@ -700,7 +702,7 @@
             .build();
     final String expected = ""
         + "LogicalProject(DEPTNO=[$7], COMM=[CAST($6):SMALLINT NOT NULL], "
-        + "$f2=[OR(SEARCH($7, Sarg[20, 30]), AND(null:NULL, =($7, 10), "
+        + "$f2=[OR(SEARCH($7, Sarg[20, 30]), AND(null, =($7, 10), "
         + "IS NULL($6), IS NULL($5)))], n2=[IS NULL($2)], "
         + "nn2=[IS NOT NULL($3)], $f5=[20], COMM0=[$6], C=[$6])\n"
         + "  LogicalTableScan(table=[[scott, EMP]])\n";
diff --git a/core/src/test/java/org/apache/calcite/test/fuzzer/RexFuzzer.java b/core/src/test/java/org/apache/calcite/test/fuzzer/RexFuzzer.java
index 9b8d1fc..3347c46 100644
--- a/core/src/test/java/org/apache/calcite/test/fuzzer/RexFuzzer.java
+++ b/core/src/test/java/org/apache/calcite/test/fuzzer/RexFuzzer.java
@@ -21,6 +21,7 @@
 import org.apache.calcite.rex.RexBuilder;
 import org.apache.calcite.rex.RexNode;
 import org.apache.calcite.rex.RexProgramBuilderBase;
+import org.apache.calcite.rex.RexUnknownAs;
 import org.apache.calcite.sql.SqlOperator;
 import org.apache.calcite.sql.fun.SqlStdOperatorTable;
 import org.apache.calcite.sql.type.SqlTypeName;
@@ -263,6 +264,8 @@
   public RexNode fuzzSearch(Random r, RexNode intExpression) {
     final RangeSet<BigDecimal> rangeSet = TreeRangeSet.create();
     final Generator<BigDecimal> integerGenerator = RexFuzzer::fuzzInt;
+    final Generator<RexUnknownAs> unknownGenerator =
+        enumGenerator(RexUnknownAs.class);
     int i = 0;
     for (;;) {
       rangeSet.add(fuzzRange(r, integerGenerator));
@@ -270,12 +273,16 @@
         break;
       }
     }
-    final boolean containsNull = r.nextInt(3) == 0;
-    final Sarg<BigDecimal> sarg = Sarg.of(containsNull, rangeSet);
-    final RexNode c = rexBuilder.makeCall(SqlStdOperatorTable.SEARCH, intExpression,
+    final Sarg<BigDecimal> sarg =
+        Sarg.of(unknownGenerator.generate(r), rangeSet);
+    return rexBuilder.makeCall(SqlStdOperatorTable.SEARCH, intExpression,
         rexBuilder.makeSearchArgumentLiteral(sarg, intExpression.getType()));
-    System.out.println(c);
-    return c;
+  }
+
+  private static <T extends Enum<T>> Generator<T> enumGenerator(
+      Class<T> enumClass) {
+    final T[] enumConstants = enumClass.getEnumConstants();
+    return r -> enumConstants[r.nextInt(enumConstants.length)];
   }
 
   <T extends Comparable<T>> Range<T> fuzzRange(Random r,
diff --git a/core/src/test/resources/org/apache/calcite/test/RelOptRulesTest.xml b/core/src/test/resources/org/apache/calcite/test/RelOptRulesTest.xml
index 44b1143..30b4367 100644
--- a/core/src/test/resources/org/apache/calcite/test/RelOptRulesTest.xml
+++ b/core/src/test/resources/org/apache/calcite/test/RelOptRulesTest.xml
@@ -11743,7 +11743,7 @@
             <![CDATA[
 LogicalProject(EMPNO=[$0])
   LogicalProject(EMPNO=[$0], ENAME=[$1], JOB=[$2], MGR=[$3], HIREDATE=[$4], SAL=[$5], COMM=[$6], DEPTNO=[$7], SLACKER=[$8])
-    LogicalFilter(condition=[<($0, CASE(OR(AND(IS NOT NULL($12), <>($9, 0)), AND(<($10, $9), null, SEARCH($9, Sarg[(-∞..0), (0..+∞)]), IS NULL($12))), 10, AND(OR(IS NULL($12), =($9, 0)), OR(>=($10, $9), =($9, 0), IS NOT NULL($12))), 20, 30))])
+    LogicalFilter(condition=[<($0, CASE(OR(AND(IS NOT NULL($12), <>($9, 0)), AND(<($10, $9), null, <>($9, 0), IS NULL($12))), 10, AND(OR(IS NULL($12), =($9, 0)), OR(>=($10, $9), =($9, 0), IS NOT NULL($12))), 20, 30))])
       LogicalJoin(condition=[=($7, $11)], joinType=[left])
         LogicalJoin(condition=[true], joinType=[inner])
           LogicalTableScan(table=[[CATALOG, SALES, EMP]])
diff --git a/druid/src/test/java/org/apache/calcite/test/DruidDateRangeRulesTest.java b/druid/src/test/java/org/apache/calcite/test/DruidDateRangeRulesTest.java
index 65ab344..9767b73 100644
--- a/druid/src/test/java/org/apache/calcite/test/DruidDateRangeRulesTest.java
+++ b/druid/src/test/java/org/apache/calcite/test/DruidDateRangeRulesTest.java
@@ -53,8 +53,8 @@
     final Fixture2 f = new Fixture2();
     checkDateRange(f,
         f.and(
-            f.le(f.timestampLiteral(2011, Calendar.JANUARY, 1), f.t),
-            f.le(f.t, f.timestampLiteral(2012, Calendar.FEBRUARY, 2))),
+            f.le(f.timestampLiteral(2011, Calendar.JANUARY, 1), f.ts),
+            f.le(f.ts, f.timestampLiteral(2012, Calendar.FEBRUARY, 2))),
         is("[2011-01-01T00:00:00.000Z/2012-02-02T00:00:00.001Z]"));
   }