Repository: GoogleCloudPlatform/cloud-bigtable-client
Pull request #1689 (2018-02-20 15:26:40)

Updating TestCheckAndMutate, with a focus on Async
- Adding a test for NOT_EQUALS null
- Allowing "NOT_EQUALS null" checks in BigtableAsyncTable
- Using synchronous calls for put and get in TestAsyncCheckAndMutate.  The tests were failing due to async puts not completing.
- Extracting ExecutionException.getCause() for exception checking in TestAsyncCheckAndMutate.

spollapally: LGTM

Patch:
diff --git a/bigtable-client-core-parent/bigtable-client-core/src/main/java/com/google/cloud/bigtable/data/v2/wrappers/Filters.java b/bigtable-client-core-parent/bigtable-client-core/src/main/java/com/google/cloud/bigtable/data/v2/wrappers/Filters.java
index c8c9a65d8..d95ca4b12 100644
--- a/bigtable-client-core-parent/bigtable-client-core/src/main/java/com/google/cloud/bigtable/data/v2/wrappers/Filters.java
+++ b/bigtable-client-core-parent/bigtable-client-core/src/main/java/com/google/cloud/bigtable/data/v2/wrappers/Filters.java
@@ -15,20 +15,22 @@
  */
 package com.google.cloud.bigtable.data.v2.wrappers;
 
-import com.google.api.client.repackaged.com.google.common.base.Preconditions;
 import com.google.api.core.InternalApi;
 import com.google.bigtable.v2.ColumnRange;
 import com.google.bigtable.v2.RowFilter;
-import com.google.bigtable.v2.TimestampRange;
 import com.google.bigtable.v2.ValueRange;
 import com.google.cloud.bigtable.data.v2.internal.RegexUtil;
+import com.google.cloud.bigtable.data.v2.wrappers.Range.AbstractByteStringRange;
+import com.google.cloud.bigtable.data.v2.wrappers.Range.AbstractTimestampRange;
+import com.google.common.base.Preconditions;
 import com.google.protobuf.ByteString;
+import javax.annotation.Nonnull;
 
 /**
  * A Fluent DSL to create a hierarchy of filters for the CheckAndMutateRow RPCs and ReadRows Query.
  *
- * <p>Intended usage is to statically import, or in case of conflict assign the static variable FILTERS
- * and use its fluent API to build filters.
+ * <p>Intended usage is to statically import, or in case of conflict, assign the static variable
+ * FILTERS and use its fluent API to build filters.
  *
  * <p>Sample code:
  *
@@ -61,7 +63,7 @@ public final class Filters {
 
   /**
    * Creates an empty chain filter list. Filters can be added to the chain by invoking {@link
-   * ChainFilter#filter(Filter)}.
+   * ChainFilter#filter(Filters.Filter)}.
    *
    * <p>The elements of "filters" are chained together to process the input row:
    *
@@ -76,7 +78,7 @@ public final class Filters {
 
   /**
    * Creates an empty interleave filter list. Filters can be added to the interleave by invoking
-   * {@link InterleaveFilter#filter(Filter)}.
+   * {@link InterleaveFilter#filter(Filters.Filter)}.
    *
    * <p>The elements of "filters" all process a copy of the input row, and the results are pooled,
    * sorted, and combined into a single output row. If multiple cells are produced with the same
@@ -89,18 +91,20 @@ public final class Filters {
 
   /**
    * Creates an empty condition filter. The filter results of the predicate can be configured by
-   * invoking {@link ConditionFilter#then(Filter)} and {@link ConditionFilter#otherwise(Filter)}.
+   * invoking {@link ConditionFilter#then(Filters.Filter)} and {@link
+   * ConditionFilter#otherwise(Filters.Filter)}.
    *
    * <p>A RowFilter which evaluates one of two possible RowFilters, depending on whether or not a
    * predicate RowFilter outputs any cells from the input row.
    *
    * <p>IMPORTANT NOTE: The predicate filter does not execute atomically with the {@link
-   * ConditionFilter#then(Filter)} and {@link ConditionFilter#otherwise(Filter)} (Filter)} filters,
-   * which may lead to inconsistent or unexpected results. Additionally, {@link ConditionFilter} may
-   * have poor performance, especially when filters are set for the {@link
-   * ConditionFilter#otherwise(Filter)}.
+   * ConditionFilter#then(Filters.Filter)} and {@link ConditionFilter#otherwise(Filters.Filter)}
+   * (Filter)} filters, which may lead to inconsistent or unexpected results. Additionally, {@link
+   * ConditionFilter} may have poor performance, especially when filters are set for the {@link
+   * ConditionFilter#otherwise(Filters.Filter)}.
    */
-  public ConditionFilter condition(Filter predicate) {
+  public ConditionFilter condition(@Nonnull Filter predicate) {
+    Preconditions.checkNotNull(predicate);
     return new ConditionFilter(predicate);
   }
 
@@ -140,6 +144,14 @@ public final class Filters {
   }
 
   // Miscellaneous filters without a clear target.
+  /**
+   * Wraps protobuf representation of a filter.
+   * <p>For advanced use only.
+   */
+  public Filter raw(RowFilter rowFilter) {
+    return new SimpleFilter(rowFilter);
+  }
+
   /** Matches all cells, regardless of input. Functionally equivalent to having no filter. */
   public Filter pass() {
     return PASS;
@@ -172,26 +184,23 @@ public final class Filters {
    * label. It is okay for an {@link InterleaveFilter} to contain multiple labels, as they will be
    * applied to separate copies of the input. This may be relaxed in the future.
    */
-  public Filter label(String label) {
+  public Filter label(@Nonnull String label) {
+    Preconditions.checkNotNull(label);
     return new SimpleFilter(RowFilter.newBuilder().setApplyLabelTransformer(label).build());
   }
 
   // Implementations of target specific filters.
   /** DSL for adding filters to a chain. */
-  public static final class ChainFilter implements Filter, Cloneable {
-
-    private final RowFilter.Chain.Builder builder;
+  public static final class ChainFilter implements Filter {
+    private RowFilter.Chain.Builder builder;
 
     private ChainFilter() {
-      this(RowFilter.Chain.newBuilder());
-    }
-
-    private ChainFilter(RowFilter.Chain.Builder builder) {
-      this.builder = builder;
+      this.builder = RowFilter.Chain.newBuilder();
     }
 
     /** Add a filter to chain. */
-    public ChainFilter filter(Filter filter) {
+    public ChainFilter filter(@Nonnull Filter filter) {
+      Preconditions.checkNotNull(filter);
       builder.addFilters(filter.toProto());
       return this;
     }
@@ -206,26 +215,30 @@ public final class Filters {
       }
     }
 
-    /**
-     * Makes a deep copy of the Chain.
-     */
+    /** Makes a deep copy of the Chain. */
     @Override
     public ChainFilter clone() {
-      return new ChainFilter(builder.build().toBuilder());
+      try {
+        ChainFilter clone = (ChainFilter) super.clone();
+        clone.builder = builder.clone();
+        return clone;
+      } catch (CloneNotSupportedException | ClassCastException e) {
+        throw new RuntimeException("should never happen");
+      }
     }
   }
 
   /** DSL for adding filters to the interleave list. */
   public static final class InterleaveFilter implements Filter {
-    RowFilter.Interleave.Builder builder = RowFilter.Interleave.newBuilder();
+    private RowFilter.Interleave.Builder builder;
 
     private InterleaveFilter() {
+      builder = RowFilter.Interleave.newBuilder();
     }
 
-    /**
-     * Adds a {@link Filter} to the interleave list.
-     */
-    public InterleaveFilter filter(Filter filter) {
+    /** Adds a {@link Filter} to the interleave list. */
+    public InterleaveFilter filter(@Nonnull Filter filter) {
+      Preconditions.checkNotNull(filter);
       builder.addFilters(filter.toProto());
       return this;
     }
@@ -239,27 +252,38 @@ public final class Filters {
         return RowFilter.newBuilder().setInterleave(builder.build()).build();
       }
     }
+
+    @Override
+    public InterleaveFilter clone() {
+      try {
+        InterleaveFilter clone = (InterleaveFilter) super.clone();
+        clone.builder = builder.clone();
+        return clone;
+      } catch (CloneNotSupportedException | ClassCastException e) {
+        throw new RuntimeException("should never happen");
+      }
+    }
   }
 
   /** DSL for configuring a conditional filter. */
   public static final class ConditionFilter implements Filter {
-    private RowFilter.Condition.Builder builder = RowFilter.Condition.newBuilder();
+    private RowFilter.Condition.Builder builder;
 
-    private ConditionFilter() {
-    }
-
-    private ConditionFilter(Filter predicate) {
-      builder.setPredicateFilter(predicate.toProto());
+    private ConditionFilter(@Nonnull Filter predicate) {
+        Preconditions.checkNotNull(predicate);
+        builder = RowFilter.Condition.newBuilder().setPredicateFilter(predicate.toProto());
     }
 
     /** Sets (replaces) the filter to apply when the predicate is true. */
-    public ConditionFilter then(Filter filter) {
+    public ConditionFilter then(@Nonnull Filter filter) {
+      Preconditions.checkNotNull(filter);
       builder.setTrueFilter(filter.toProto());
       return this;
     }
 
     /** Sets (replaces) the filter to apply when the predicate is false. */
-    public ConditionFilter otherwise(Filter filter) {
+    public ConditionFilter otherwise(@Nonnull Filter filter) {
+      Preconditions.checkNotNull(filter);
       builder.setFalseFilter(filter.toProto());
       return this;
     }
@@ -267,47 +291,61 @@ public final class Filters {
     @InternalApi
     @Override
     public RowFilter toProto() {
+      Preconditions.checkState(
+          builder.hasTrueFilter() || builder.hasFalseFilter(),
+          "ConditionFilter must have either a then or otherwise filter.");
       return RowFilter.newBuilder().setCondition(builder.build()).build();
     }
+
+    @Override
+    public ConditionFilter clone() {
+      try {
+        ConditionFilter clone = (ConditionFilter) super.clone();
+        clone.builder = builder.clone();
+        return clone;
+      } catch (CloneNotSupportedException | ClassCastException e) {
+        throw new RuntimeException("should never happen");
+      }
+    }
   }
 
   public static final class KeyFilter {
-
-    private KeyFilter() {
-    }
+    private KeyFilter() {}
 
     /**
      * Matches only cells from rows whose keys satisfy the given <a
-     * href="https://github.com/google/re2/wiki/Syntax">RE2 regex</a>. In other words, passes through
-     * the entire row when the key matches, and otherwise produces an empty row. Note that, since
-     * row keys can contain arbitrary bytes, the `\C` escape sequence must be used if a true
+     * href="https://github.com/google/re2/wiki/Syntax">RE2 regex</a>. In other words, passes
+     * through the entire row when the key matches, and otherwise produces an empty row. Note that,
+     * since row keys can contain arbitrary bytes, the `\C` escape sequence must be used if a true
      * wildcard is desired. The `.` character will not match the new line character `\n`, which may
      * be present in a binary key.
      */
-    public Filter regex(String regex) {
+    public Filter regex(@Nonnull String regex) {
+      Preconditions.checkNotNull(regex);
       return regex(ByteString.copyFromUtf8(regex));
     }
 
     /**
      * Matches only cells from rows whose keys satisfy the given <a
-     * href="https://github.com/google/re2/wiki/Syntax">RE2 regex</a>. In other words, passes through
-     * the entire row when the key matches, and otherwise produces an empty row. Note that, since
-     * row keys can contain arbitrary bytes, the `\C` escape sequence must be used if a true
+     * href="https://github.com/google/re2/wiki/Syntax">RE2 regex</a>. In other words, passes
+     * through the entire row when the key matches, and otherwise produces an empty row. Note that,
+     * since row keys can contain arbitrary bytes, the `\C` escape sequence must be used if a true
      * wildcard is desired. The `.` character will not match the new line character `\n`, which may
      * be present in a binary key.
      */
-    public Filter regex(ByteString regex) {
-      return new SimpleFilter(
-          RowFilter.newBuilder().setRowKeyRegexFilter(regex).build());
+    public Filter regex(@Nonnull ByteString regex) {
+      Preconditions.checkNotNull(regex);
+      return new SimpleFilter(RowFilter.newBuilder().setRowKeyRegexFilter(regex).build());
     }
 
     /**
      * Matches only cells from rows whose keys equal the value. In other words, passes through the
      * entire row when the key matches, and otherwise produces an empty row.
      */
-    public Filter exactMatch(ByteString value) {
-      return new SimpleFilter(
-          RowFilter.newBuilder().setRowKeyRegexFilter(RegexUtil.literalRegex(value)).build());
+    public Filter exactMatch(@Nonnull ByteString value) {
+      Preconditions.checkNotNull(value);
+
+      return regex(RegexUtil.literalRegex(value));
     }
 
     /**
@@ -315,90 +353,37 @@ public final class Filters {
      * probability 1-`probability`.
      */
     public Filter sample(double probability) {
+      Preconditions.checkArgument(0 <= probability, "Probability must be positive");
+      Preconditions.checkArgument(probability <= 1.0, "Probability must be less than 1.0");
+
       return new SimpleFilter(RowFilter.newBuilder().setRowSampleFilter(probability).build());
     }
   }
 
   public static final class FamilyFilter {
-
-    private FamilyFilter() {
-    }
+    private FamilyFilter() {}
 
     /**
-     * Matches only cells from columns whose families satisfy the given
-     * <a href="https://github.com/google/re2/wiki/Syntax">RE2 regex</a>. For technical reasons, the
+     * Matches only cells from columns whose families satisfy the given <a
+     * href="https://github.com/google/re2/wiki/Syntax">RE2 regex</a>. For technical reasons, the
      * regex must not contain the `:` character, even if it is not being used as a literal. Note
      * that, since column families cannot contain the new line character `\n`, it is sufficient to
      * use `.` as a full wildcard when matching column family names.
      */
-    public Filter regex(String regex) {
+    public Filter regex(@Nonnull String regex) {
+      Preconditions.checkNotNull(regex);
       return new SimpleFilter(RowFilter.newBuilder().setFamilyNameRegexFilter(regex).build());
     }
 
-    public Filter regex(ByteString regex) {
-      return new SimpleFilter(RowFilter.newBuilder().setFamilyNameRegexFilterBytes(regex).build());
-    }
-
     /** Matches only cells from columns whose families match the value. */
-    public Filter exactMatch(String value) {
-      return new SimpleFilter(
-          RowFilter.newBuilder().setFamilyNameRegexFilter(RegexUtil.literalRegex(value)).build());
-    }
-  }
-
-  /**
-   * Matches only cells from columns within the given range.
-   */
-  public static final class QualifierRangeFilter implements Filter{
-    private ColumnRange.Builder range = ColumnRange.newBuilder();
-
-    private QualifierRangeFilter(String family) {
-      range.setFamilyName(Preconditions.checkNotNull(family));
-    }
-
-    /**
-     * Used when giving an inclusive lower bound for the range.
-     */
-    public QualifierRangeFilter startClosed(ByteString value) {
-      range.setStartQualifierClosed(value);
-      return this;
-    }
-
-    /**
-     * Used when giving an exclusive lower bound for the range.
-     */
-    public QualifierRangeFilter startOpen(ByteString value) {
-      range.setStartQualifierOpen(value);
-      return this;
-    }
-
-    /**
-     * sed when giving an inclusive upper bound for the range.
-     */
-    public QualifierRangeFilter endClosed(ByteString value) {
-      range.setEndQualifierClosed(value);
-      return this;
-    }
-
-    /**
-     * Used when giving an exclusive upper bound for the range.
-     */
-    public QualifierRangeFilter endOpen(ByteString value) {
-      range.setEndQualifierOpen(value);
-      return this;
-    }
-
-    @InternalApi
-    @Override
-    public RowFilter toProto() {
-      return RowFilter.newBuilder().setColumnRangeFilter(range.build()).build();
+    public Filter exactMatch(@Nonnull String value) {
+      Preconditions.checkNotNull(value);
+      return regex(RegexUtil.literalRegex(value));
     }
   }
 
   public static final class QualifierFilter {
-
-    private QualifierFilter() {
-    }
+    private QualifierFilter() {}
 
     /**
      * Matches only cells from columns whose qualifiers satisfy the given <a
@@ -407,7 +392,8 @@ public final class Filters {
      * wildcard is desired. The `.` character will not match the new line character `\n`, which may
      * be present in a binary qualifier.
      */
-    public Filter regex(String regex) {
+    public Filter regex(@Nonnull String regex) {
+      Preconditions.checkNotNull(regex);
       return regex(ByteString.copyFromUtf8(regex));
     }
 
@@ -418,69 +404,80 @@ public final class Filters {
      * wildcard is desired. The `.` character will not match the new line character `\n`, which may
      * be present in a binary qualifier.
      */
-    public Filter regex(ByteString regex) {
-      return new SimpleFilter(
-          RowFilter.newBuilder()
-              .setColumnQualifierRegexFilter(regex)
-              .build());
+    public Filter regex(@Nonnull ByteString regex) {
+      Preconditions.checkNotNull(regex);
+
+      return new SimpleFilter(RowFilter.newBuilder().setColumnQualifierRegexFilter(regex).build());
     }
 
     /** Matches only cells from columns whose qualifiers equal the value. */
-    public Filter exactMatch(ByteString value) {
+    public Filter exactMatch(@Nonnull ByteString value) {
+      Preconditions.checkNotNull(value);
       return regex(RegexUtil.literalRegex(value));
     }
 
     /**
      * Construct a {@link QualifierRangeFilter} that can create a {@link ColumnRange} oriented
      * {@link Filter}.
+     *
      * @return a new {@link QualifierRangeFilter}
      */
-    public QualifierRangeFilter rangeWithinFamily(String family) {
+    public QualifierRangeFilter rangeWithinFamily(@Nonnull String family) {
+      Preconditions.checkNotNull(family);
       return new QualifierRangeFilter(family);
     }
   }
 
-  /**
-   * Matches only cells with microsecond timestamps within the given range.  Start is inclusive
-   * and end is exclusive.
-   */
-  public static final class TimestampRangeFilter implements Filter {
-    private final TimestampRange.Builder range = TimestampRange.newBuilder();
+  /** Matches only cells from columns within the given range. */
+  public static final class QualifierRangeFilter
+      extends AbstractByteStringRange<QualifierRangeFilter> implements Filter {
+    private final String family;
 
-    private TimestampRangeFilter() {
+    private QualifierRangeFilter(String family) {
+      this.family = family;
     }
 
-    /**
-     * Inclusive lower bound. If left empty, interpreted as 0.
-     *
-     * @param startMicros inclusive timestamp in microseconds.
-     */
-    public TimestampRangeFilter startClosed(long startMicros) {
-      range.setStartTimestampMicros(startMicros);
-      return this;
-    }
+    @InternalApi
+    @Override
+    public RowFilter toProto() {
+      ColumnRange.Builder builder = ColumnRange.newBuilder().setFamilyName(family);
+
+      switch (getStartBound()) {
+        case CLOSED:
+          builder.setStartQualifierClosed(getStart());
+          break;
+        case OPEN:
+          builder.setStartQualifierOpen(getStart());
+          break;
+        case UNBOUNDED:
+          break;
+        default:
+          throw new IllegalStateException("Unknown start bound: " + getStartBound());
+      }
+      switch (getEndBound()) {
+        case CLOSED:
+          builder.setEndQualifierClosed(getEnd());
+          break;
+        case OPEN:
+          builder.setEndQualifierOpen(getEnd());
+          break;
+        case UNBOUNDED:
+          break;
+        default:
+          throw new IllegalStateException("Unknown end bound: " + getEndBound());
+      }
 
-    /**
-     * Exclusive upper bound. If left empty, interpreted as infinity.
-     *
-     * @param endMicros exclusive timestamp in microseconds.
-     */
-    public TimestampRangeFilter endOpen(long endMicros) {
-      range.setEndTimestampMicros(endMicros);
-      return this;
+      return RowFilter.newBuilder().setColumnRangeFilter(builder.build()).build();
     }
 
-    @InternalApi
     @Override
-    public RowFilter toProto() {
-      return RowFilter.newBuilder().setTimestampRangeFilter(range.build()).build();
+    public QualifierRangeFilter clone() {
+      return super.clone();
     }
   }
 
   public static final class TimestampFilter {
-
-    private TimestampFilter() {
-    }
+    private TimestampFilter() {}
 
     /**
      * Matches only cells with timestamps within the given range.
@@ -490,68 +487,56 @@ public final class Filters {
     public TimestampRangeFilter range() {
       return new TimestampRangeFilter();
     }
-
-    /**
-     * Matches only cells with timestamps within the given range.
-     *
-     * @param startMicros Inclusive start of the range in microseconds.
-     * @param endMicros Exclusive end of the range in microseconds.
-     */
-    public Filter range(long startMicros, long endMicros) {
-      return range().startClosed(startMicros).endOpen(endMicros);
-    }
   }
 
-  /** Matches only cells with values that fall within the given value range. */
-  public static final class ValueRangeFilter implements Filter{
-    private ValueRange.Builder range = ValueRange.newBuilder();
-
-    private ValueRangeFilter() {
-    }
-
-    /**
-     * Used when giving an inclusive lower bound for the range.
-     */
-    public ValueRangeFilter startClosed(ByteString value) {
-      range.setStartValueClosed(value);
-      return this;
-    }
-
-    /**
-     * Used when giving an exclusive lower bound for the range.
-     */
-    public ValueRangeFilter startOpen(ByteString value) {
-      range.setStartValueOpen(value);
-      return this;
-    }
-
-    /**
-     * Used when giving an inclusive upper bound for the range.
-     */
-    public ValueRangeFilter endClosed(ByteString value) {
-      range.setEndValueClosed(value);
-      return this;
-    }
-
-    public ValueRangeFilter endOpen(ByteString value) {
-      /**
-       * Used when giving an exclusive upper bound for the range.
-       */
-      range.setEndValueOpen(value);
-      return this;
-    }
+  /**
+   * Matches only cells with microsecond timestamps within the given range.
+   */
+  public static final class TimestampRangeFilter
+      extends AbstractTimestampRange<TimestampRangeFilter> implements Filter {
+    private TimestampRangeFilter() {}
 
     @InternalApi
     @Override
     public RowFilter toProto() {
-      return RowFilter.newBuilder().setValueRangeFilter(range.build()).build();
+      com.google.bigtable.v2.TimestampRange.Builder builder =
+          com.google.bigtable.v2.TimestampRange.newBuilder();
+
+      switch (getStartBound()) {
+        case CLOSED:
+          builder.setStartTimestampMicros(getStart());
+          break;
+        case OPEN:
+          builder.setStartTimestampMicros(getStart() + 1);
+          break;
+        case UNBOUNDED:
+          break;
+        default:
+          throw new IllegalStateException("Unknown start bound: " + getStartBound());
+      }
+      switch (getEndBound()) {
+        case CLOSED:
+          builder.setEndTimestampMicros(getEnd() + 1);
+          break;
+        case OPEN:
+          builder.setEndTimestampMicros(getEnd());
+          break;
+        case UNBOUNDED:
+          break;
+        default:
+          throw new IllegalStateException("Unknown end bound: " + getEndBound());
+      }
+      return RowFilter.newBuilder().setTimestampRangeFilter(builder.build()).build();
+    }
+
+    @Override
+    public TimestampRangeFilter clone() {
+      return super.clone();
     }
   }
 
   public static final class ValueFilter {
-
-    private ValueFilter() {
-    }
+    private ValueFilter() {}
 
     /**
      * Matches only cells with values that satisfy the given <a
@@ -559,16 +544,15 @@ public final class Filters {
      * can contain arbitrary bytes, the `\C` escape sequence must be used if a true wildcard is
      * desired. The `.` character will not match the new line character `\n`, which may be present
      * in a binary value.
-     *
-     * @param regex
-     * @return
      */
-    public Filter regex(String regex) {
+    public Filter regex(@Nonnull String regex) {
+      Preconditions.checkNotNull(regex);
       return regex(ByteString.copyFromUtf8(regex));
     }
 
     /** Matches only cells with values that match the given value. */
-    public Filter exactMatch(ByteString value) {
+    public Filter exactMatch(@Nonnull ByteString value) {
+      Preconditions.checkNotNull(value);
       return regex(RegexUtil.literalRegex(value));
     }
 
@@ -578,17 +562,16 @@ public final class Filters {
      * can contain arbitrary bytes, the `\C` escape sequence must be used if a true wildcard is
      * desired. The `.` character will not match the new line character `\n`, which may be present
      * in a binary value.
-     *
-     * @param regex
-     * @return
      */
-    public Filter regex(ByteString regex) {
+    public Filter regex(@Nonnull ByteString regex) {
+      Preconditions.checkNotNull(regex);
       return new SimpleFilter(RowFilter.newBuilder().setValueRegexFilter(regex).build());
     }
 
     /**
-     * Construct a {@link ValueRangeFilter} that can create a {@link ValueRange} oriented
-     * {@link Filter}.
+     * Construct a {@link ValueRangeFilter} that can create a {@link ValueRange} oriented {@link
+     * Filter}.
+     *
      * @return a new {@link ValueRangeFilter}
      */
     public ValueRangeFilter range() {
@@ -601,10 +584,50 @@ public final class Filters {
     }
   }
 
-  public static final class OffsetFilter {
+  /** Matches only cells with values that fall within the given value range. */
+  public static final class ValueRangeFilter extends AbstractByteStringRange<ValueRangeFilter>
+      implements Filter {
+    private ValueRangeFilter() {}
+
+    @InternalApi
+    @Override
+    public RowFilter toProto() {
+      ValueRange.Builder builder = ValueRange.newBuilder();
+      switch (getStartBound()) {
+        case CLOSED:
+          builder.setStartValueClosed(getStart());
+          break;
+        case OPEN:
+          builder.setStartValueOpen(getStart());
+          break;
+        case UNBOUNDED:
+          break;
+        default:
+          throw new IllegalStateException("Unknown start bound: " + getStartBound());
+      }
+      switch (getEndBound()) {
+        case CLOSED:
+          builder.setEndValueClosed(getEnd());
+          break;
+        case OPEN:
+          builder.setEndValueOpen(getEnd());
+          break;
+        case UNBOUNDED:
+          break;
+        default:
+          throw new IllegalStateException("Unknown end bound: " + getEndBound());
+      }
+      return RowFilter.newBuilder().setValueRangeFilter(builder.build()).build();
+    }
 
-    private OffsetFilter() {
+    @Override
+    public ValueRangeFilter clone() {
+      return super.clone();
     }
+  }
+
+  public static final class OffsetFilter {
+    private OffsetFilter() {}
 
     /**
      * Skips the first N cells of each row, matching all subsequent cells. If duplicate cells are
@@ -617,9 +640,7 @@ public final class Filters {
   }
 
   public static final class LimitFilter {
-
-    private LimitFilter() {
-    }
+    private LimitFilter() {}
 
     /**
      * Matches only the first N cells of each row. If duplicate cells are present, as is possible
@@ -642,10 +663,10 @@ public final class Filters {
   }
 
   private static final class SimpleFilter implements Filter {
-
     private final RowFilter proto;
 
-    private SimpleFilter(RowFilter proto) {
+    private SimpleFilter(@Nonnull RowFilter proto) {
+      Preconditions.checkNotNull(proto);
       this.proto = proto;
     }
 
@@ -654,14 +675,19 @@ public final class Filters {
     public RowFilter toProto() {
       return proto;
     }
-  }
 
-  public interface Filter {
-    @InternalApi
-    public abstract RowFilter toProto();
+    @Override
+    public SimpleFilter clone() {
+      try {
+        return (SimpleFilter) super.clone();
+      } catch (CloneNotSupportedException e) {
+        throw new RuntimeException("should never happen", e);
+      }
+    }
   }
 
-  public Filter raw(final RowFilter rowFilter) {
-    return new SimpleFilter(rowFilter);
+  public interface Filter extends Cloneable {
+    @InternalApi
+    RowFilter toProto();
   }
 }
diff --git a/bigtable-client-core-parent/bigtable-client-core/src/main/java/com/google/cloud/bigtable/data/v2/wrappers/Range.java b/bigtable-client-core-parent/bigtable-client-core/src/main/java/com/google/cloud/bigtable/data/v2/wrappers/Range.java
new file mode 100644
index 000000000..6c5996976
--- /dev/null
+++ b/bigtable-client-core-parent/bigtable-client-core/src/main/java/com/google/cloud/bigtable/data/v2/wrappers/Range.java
@@ -0,0 +1,267 @@
+/*
+ * Copyright 2018 Google LLC
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.cloud.bigtable.data.v2.wrappers;
+
+import com.google.common.base.Preconditions;
+import com.google.protobuf.ByteString;
+import javax.annotation.Nonnull;
+
+/**
+ * Range API.
+ *
+ * <p>This base class represents the API for all ranges in the Cloud Bigtable client. Please note
+ * this mutable type. It's intended to support fluent DSLs.For example:
+ *
+ * <pre>{@code
+ * // A Range that encloses all strings
+ * ByteStringRange.unbounded();
+ *
+ * // Range that includes all strings including "begin" up until "end"
+ * ByteStringRange.unbounded().of("start", "end");
+ *
+ * // Create a Bytestring range with an unbounded start and the inclusive end "end"
+ * ByteStringRange.unbounded().endClosed("end");
+ *
+ * // Ranges are mutable, so take care to clone them to get a new instance
+ * ByteStringRange r1 = ByteStringRange.of("a", "z");
+ * ByteStringRange r2 = r1.clone().endUnbounded();
+ * }</pre>
+ */
+abstract class Range<T, R extends Range<T, R>> {
+  public enum BoundType {
+    OPEN,
+    CLOSED,
+    UNBOUNDED
+  }
+
+  private BoundType startBound;
+  private T start;
+  private BoundType endBound;
+  private T end;
+
+  Range() {
+    this(BoundType.UNBOUNDED, null, BoundType.UNBOUNDED, null);
+  }
+
+  Range(BoundType startBound, T start, BoundType endBound, T end) {
+    this.startBound = startBound;
+    this.start = start;
+    this.endBound = endBound;
+    this.end = end;
+  }
+
+  /**
+   * Creates a new {@link Range} with the specified inclusive start and the specified exclusive end.
+   */
+  public R of(@Nonnull T startClosed, @Nonnull T endOpen) {
+    return startClosed(startClosed).endOpen(endOpen);
+  }
+
+  /** Creates a new {@link Range} with an unbounded start and the current end. */
+  public R startUnbounded() {
+    this.start = null;
+    this.startBound = BoundType.UNBOUNDED;
+    return thisT();
+  }
+
+  /** Creates a new {@link Range} with the specified exclusive start and the current end. */
+  public R startOpen(@Nonnull T start) {
+    this.start = Preconditions.checkNotNull(start, "Start can't be null");
+    this.startBound = BoundType.OPEN;
+    return thisT();
+  }
+
+  /** Creates a new {@link Range} with the specified inclusive start and the current end. */
+  public R startClosed(@Nonnull T start) {
+    this.start = Preconditions.checkNotNull(start, "Start can't be null");
+    this.startBound = BoundType.CLOSED;
+    return thisT();
+  }
+
+  /** Creates a new {@link Range} with the current start and an unbounded end. */
+  public R endUnbounded() {
+    this.end = null;
+    this.endBound = BoundType.UNBOUNDED;
+    return thisT();
+  }
+
+  /** Creates a new {@link Range} with the specified exclusive end and the current start. */
+  public R endOpen(@Nonnull T end) {
+    this.end = Preconditions.checkNotNull(end, "End can't be null");
+    this.endBound = BoundType.OPEN;
+    return thisT();
+  }
+
+  /** Creates a new {@link Range} with the specified inclusive end and the current start. */
+  public R endClosed(@Nonnull T end) {
+    this.end = Preconditions.checkNotNull(end, "End can't be null");
+    this.endBound = BoundType.CLOSED;
+    return thisT();
+  }
+
+  /** Gets the current start {@link BoundType}. */
+  public BoundType getStartBound() {
+    return startBound;
+  }
+
+  /**
+   * Gets the current start value.
+   *
+   * @throws IllegalStateException If the current {@link #getStartBound()} is {@link
+   *     BoundType#UNBOUNDED}.
+   */
+  public T getStart() {
+    Preconditions.checkState(startBound != BoundType.UNBOUNDED, "Start is unbounded");
+    return start;
+  }
+
+  /** Gets the current end {@link BoundType}. */
+  public BoundType getEndBound() {
+    return endBound;
+  }
+
+  /**
+   * Gets the current end value.
+   *
+   * @throws IllegalStateException If the current {@link #getEndBound()} is {@link
+   *     BoundType#UNBOUNDED}.
+   */
+  public T getEnd() {
+    Preconditions.checkState(endBound != BoundType.UNBOUNDED, "End is unbounded");
+    return end;
+  }
+
+  @SuppressWarnings("unchecked")
+  private R thisT() {
+    return (R) this;
+  }
+
+  /** Abstract specialization of a {@link Range} for timestamps. */
+  abstract static class AbstractTimestampRange<R extends AbstractTimestampRange<R>>
+      extends Range<Long, R> implements Cloneable {
+    AbstractTimestampRange() {
+      super();
+    }
+
+    AbstractTimestampRange(BoundType startBound, Long start, BoundType endBound, Long end) {
+      super(startBound, start, endBound, end);
+    }
+
+    @SuppressWarnings("unchecked")
+    @Override
+    protected R clone() {
+      try {
+        return (R) super.clone();
+      } catch (CloneNotSupportedException e) {
+        throw new RuntimeException("should never happen", e);
+      }
+    }
+  }
+
+  /**
+   * Abstract specialization of a {@link Range} for {@link ByteString}s. Allows for easy interop
+   * with simple Strings.
+   */
+  abstract static class AbstractByteStringRange<R extends AbstractByteStringRange<R>>
+      extends Range<ByteString, R> implements Cloneable {
+    AbstractByteStringRange() {
+      this(BoundType.UNBOUNDED, null, BoundType.UNBOUNDED, null);
+    }
+
+    AbstractByteStringRange(
+        BoundType startBound, ByteString start, BoundType endBound, ByteString end) {
+      super(startBound, start, endBound, end);
+    }
+
+    /**
+     * Creates a new {@link Range} with the specified inclusive start and the specified exclusive
+     * end.
+     */
+    public R of(String startClosed, String endOpen) {
+      return of(wrap(startClosed), wrap(endOpen));
+    }
+
+    /** Creates a new {@link Range} with the specified exclusive start and the current end. */
+    public R startOpen(String start) {
+      return startOpen(wrap(start));
+    }
+
+    /** Creates a new {@link Range} with the specified inclusive start and the current end. */
+    public R startClosed(String start) {
+      return startClosed(wrap(start));
+    }
+
+    /** Creates a new {@link Range} with the specified exclusive end and the current start. */
+    public R endOpen(String end) {
+      return endOpen(wrap(end));
+    }
+
+    /** Creates a new {@link Range} with the specified inclusive end and the current start. */
+    public R endClosed(String end) {
+      return endClosed(wrap(end));
+    }
+
+    @SuppressWarnings("unchecked")
+    @Override
+    protected R clone() {
+      try {
+        return (R) super.clone();
+      } catch (CloneNotSupportedException e) {
+        throw new RuntimeException("should never happen", e);
+      }
+    }
+
+    static ByteString wrap(String str) {
+      return ByteString.copyFromUtf8(str);
+    }
+  }
+
+  /** Concrete Range for timestamps */
+  public static final class TimestampRange extends AbstractTimestampRange<TimestampRange> {
+    public static TimestampRange unbounded() {
+      return new TimestampRange(BoundType.UNBOUNDED, null, BoundType.UNBOUNDED, null);
+    }
+
+    public static TimestampRange create(long closedStart, long openEnd) {
+      return new TimestampRange(BoundType.CLOSED, closedStart, BoundType.OPEN, openEnd);
+    }
+
+    private TimestampRange(BoundType startBound, Long start, BoundType endBound, Long end) {
+      super(startBound, start, endBound, end);
+    }
+  }
+
+  /** Concrete Range for ByteStrings */
+  public static final class ByteStringRange extends AbstractByteStringRange<ByteStringRange> {
+    public static ByteStringRange unbounded() {
+      return new ByteStringRange(BoundType.UNBOUNDED, null, BoundType.UNBOUNDED, null);
+    }
+
+    public static ByteStringRange create(ByteString closedStart, ByteString openEnd) {
+      return new ByteStringRange(BoundType.CLOSED, closedStart, BoundType.OPEN, openEnd);
+    }
+
+    public static ByteStringRange create(String closedStart, String openEnd) {
+      return new ByteStringRange(
+          BoundType.CLOSED, wrap(closedStart), BoundType.OPEN, wrap(openEnd));
+    }
+
+    private ByteStringRange(
+        BoundType startBound, ByteString start, BoundType endBound, ByteString end) {
+      super(startBound, start, endBound, end);
+    }
+  }
+}
diff --git a/bigtable-hbase-2.x-parent/bigtable-hbase-2.x/src/main/java/com/google/cloud/bigtable/hbase2_x/BigtableAsyncTable.java b/bigtable-hbase-2.x-parent/bigtable-hbase-2.x/src/main/java/com/google/cloud/bigtable/hbase2_x/BigtableAsyncTable.java
index 82c72264e..52da95649 100644
--- a/bigtable-hbase-2.x-parent/bigtable-hbase-2.x/src/main/java/com/google/cloud/bigtable/hbase2_x/BigtableAsyncTable.java
+++ b/bigtable-hbase-2.x-parent/bigtable-hbase-2.x/src/main/java/com/google/cloud/bigtable/hbase2_x/BigtableAsyncTable.java
@@ -162,7 +162,10 @@ public class BigtableAsyncTable implements AsyncTable {
     @Override
     public CheckAndMutateBuilder ifMatches(CompareOperator compareOp, byte[] value) {
       this.op = Preconditions.checkNotNull(compareOp, "compareOp is null");
-      this.value = Preconditions.checkNotNull(value, "value is null");
+      if (compareOp != CompareOperator.EQUAL && compareOp != CompareOperator.NOT_EQUAL) {
+        this.value =
+            Preconditions.checkNotNull(value, "value is null for compareOperator: " + compareOp);
+      }
       return this;
     }
 
@@ -173,7 +176,6 @@ public class BigtableAsyncTable implements AsyncTable {
 
     @Override
     public CompletableFuture<Boolean> thenPut(Put put) {
-      preCheck();
       try {
         return call(put.getRow(), hbaseAdapter.adapt(put));
       } catch (Exception e) {
@@ -181,25 +183,8 @@ public class BigtableAsyncTable implements AsyncTable {
       }
     }
 
-    private CompletableFuture<Boolean> call(byte[] actionRow, MutateRowRequest mutateRowRequest)
-        throws IOException {
-      CheckAndMutateRowRequest request =
-          CheckAndMutateUtil.makeConditionalMutationRequest(
-              hbaseAdapter,
-              row,
-              family,
-              qualifier,
-              BigtableTable.toCompareOp(op),
-              value,
-              actionRow,
-              mutateRowRequest.getMutationsList());
-      return client.checkAndMutateRowAsync(request).thenApply(
-        response -> CheckAndMutateUtil.wasMutationApplied(request, response));
-    }
-
     @Override
     public CompletableFuture<Boolean> thenDelete(Delete delete) {
-      preCheck();
       try {
         return call(delete.getRow(), hbaseAdapter.adapt(delete));
       } catch (Exception e) {
@@ -209,13 +194,29 @@ public class BigtableAsyncTable implements AsyncTable {
 
     @Override
     public CompletableFuture<Boolean> thenMutate(RowMutations mutation) {
-      preCheck();
       try {
         return call(mutation.getRow(), hbaseAdapter.adapt(mutation));
       } catch (Exception e) {
         return FutureUtils.failedFuture(e);
       }
     }
+
+    private CompletableFuture<Boolean> call(byte[] actionRow, MutateRowRequest mutateRowRequest)
+        throws IOException {
+      preCheck();
+      CheckAndMutateRowRequest request =
+          CheckAndMutateUtil.makeConditionalMutationRequest(
+              hbaseAdapter,
+              row,
+              family,
+              qualifier,
+              BigtableTable.toCompareOp(op),
+              value,
+              actionRow,
+              mutateRowRequest.getMutationsList());
+      return client.checkAndMutateRowAsync(request).thenApply(
+        response -> CheckAndMutateUtil.wasMutationApplied(request, response));
+    }
   }
 
   @Override
diff --git a/bigtable-hbase-parent/bigtable-hbase/src/main/java/com/google/cloud/bigtable/hbase/adapters/filters/TimestampFilterUtil.java b/bigtable-hbase-parent/bigtable-hbase/src/main/java/com/google/cloud/bigtable/hbase/adapters/filters/TimestampFilterUtil.java
index 9f4d1f140..a8b5e6119 100644
--- a/bigtable-hbase-parent/bigtable-hbase/src/main/java/com/google/cloud/bigtable/hbase/adapters/filters/TimestampFilterUtil.java
+++ b/bigtable-hbase-parent/bigtable-hbase/src/main/java/com/google/cloud/bigtable/hbase/adapters/filters/TimestampFilterUtil.java
@@ -48,6 +48,6 @@ public class TimestampFilterUtil {
    */
   public static Filter toTimestampRangeFilter(long bigtableStartTimestamp,
       long bigtableEndTimestamp) {
-    return FILTERS.timestamp().range(bigtableStartTimestamp, bigtableEndTimestamp);
+    return FILTERS.timestamp().range().of(bigtableStartTimestamp, bigtableEndTimestamp);
   }
 }
diff --git a/bigtable-hbase-parent/bigtable-hbase/src/main/java/com/google/cloud/bigtable/hbase/adapters/read/ScanAdapter.java b/bigtable-hbase-parent/bigtable-hbase/src/main/java/com/google/cloud/bigtable/hbase/adapters/read/ScanAdapter.java
index 2b292b00d..51c93cdcc 100644
--- a/bigtable-hbase-parent/bigtable-hbase/src/main/java/com/google/cloud/bigtable/hbase/adapters/read/ScanAdapter.java
+++ b/bigtable-hbase-parent/bigtable-hbase/src/main/java/com/google/cloud/bigtable/hbase/adapters/read/ScanAdapter.java
@@ -189,7 +189,7 @@ public class ScanAdapter implements ReadOperationAdapter<Scan> {
   }
 
   private Filters.Filter createFamilyFilter(byte[] familyName) {
-    return FILTERS.family().regex(ByteStringer.wrap(quoteRegex(familyName)));
+    return FILTERS.family().exactMatch(new String(familyName));
   }
 
   private Filters.Filter createColumnLimitFilter(int maxVersionsPerColumn) {


Test patch:
diff --git a/bigtable-client-core-parent/bigtable-client-core/src/test/java/com/google/cloud/bigtable/data/v2/wrappers/FiltersTest.java b/bigtable-client-core-parent/bigtable-client-core/src/test/java/com/google/cloud/bigtable/data/v2/wrappers/FiltersTest.java
index f8032f967..d4737114c 100644
--- a/bigtable-client-core-parent/bigtable-client-core/src/test/java/com/google/cloud/bigtable/data/v2/wrappers/FiltersTest.java
+++ b/bigtable-client-core-parent/bigtable-client-core/src/test/java/com/google/cloud/bigtable/data/v2/wrappers/FiltersTest.java
@@ -17,16 +17,14 @@ package com.google.cloud.bigtable.data.v2.wrappers;
 
 import static com.google.cloud.bigtable.data.v2.wrappers.Filters.FILTERS;
 
-import java.util.concurrent.TimeUnit;
-
 import com.google.bigtable.v2.ColumnRange;
 import com.google.bigtable.v2.RowFilter;
 import com.google.bigtable.v2.RowFilter.Chain;
 import com.google.bigtable.v2.RowFilter.Condition;
 import com.google.bigtable.v2.RowFilter.Interleave;
+import com.google.bigtable.v2.TimestampRange;
 import com.google.bigtable.v2.ValueRange;
 import com.google.protobuf.ByteString;
-
 import org.junit.Assert;
 import org.junit.Test;
 import org.junit.runner.RunWith;
@@ -37,12 +35,11 @@ public class FiltersTest {
   @Test
   public void chainTest() {
     RowFilter actualProto =
-        FILTERS.chain()
+        FILTERS
+            .chain()
             .filter(FILTERS.key().regex(".*"))
             .filter(FILTERS.key().sample(0.5))
-            .filter(FILTERS.chain()
-                .filter(FILTERS.family().regex("hi$"))
-                .filter(FILTERS.qualifier().regex("^q")))
+            .filter(FILTERS.chain().filter(FILTERS.family().regex("hi$")).filter(FILTERS.pass()))
             .toProto();
 
     RowFilter expectedFilter =
@@ -58,9 +55,7 @@ public class FiltersTest {
                                 Chain.newBuilder()
                                     .addFilters(
                                         RowFilter.newBuilder().setFamilyNameRegexFilter("hi$"))
-                                    .addFilters(
-                                        RowFilter.newBuilder().setColumnQualifierRegexFilter(
-                                            ByteString.copyFromUtf8("^q"))))))
+                                    .addFilters(RowFilter.newBuilder().setPassAllFilter(true)))))
             .build();
 
     Assert.assertEquals(expectedFilter, actualProto);
@@ -69,12 +64,12 @@ public class FiltersTest {
   @Test
   public void interleaveTest() {
     RowFilter actualProto =
-        FILTERS.interleave()
+        FILTERS
+            .interleave()
             .filter(FILTERS.key().regex(".*"))
             .filter(FILTERS.key().sample(0.5))
-            .filter(FILTERS.interleave()
-                .filter(FILTERS.family().regex("hi$"))
-                .filter(FILTERS.qualifier().regex("^q")))
+            .filter(
+                FILTERS.interleave().filter(FILTERS.family().regex("hi$")).filter(FILTERS.pass()))
             .toProto();
 
     RowFilter expectedFilter =
@@ -91,8 +86,7 @@ public class FiltersTest {
                                     .addFilters(
                                         RowFilter.newBuilder().setFamilyNameRegexFilter("hi$"))
                                     .addFilters(
-                                        RowFilter.newBuilder().setColumnQualifierRegexFilter(
-                                            ByteString.copyFromUtf8("^q"))))))
+                                        RowFilter.newBuilder().setPassAllFilter(true).build()))))
             .build();
 
     Assert.assertEquals(expectedFilter, actualProto);
@@ -100,8 +94,9 @@ public class FiltersTest {
 
   @Test
   public void conditionTest() {
-    RowFilter actualProto =
-        FILTERS.condition(FILTERS.key().regex(".*"))
+    RowFilter actualFilter =
+        FILTERS
+            .condition(FILTERS.key().regex(".*"))
             .then(FILTERS.label("true"))
             .otherwise(FILTERS.label("false"))
             .toProto();
@@ -116,173 +111,217 @@ public class FiltersTest {
                     .setFalseFilter(RowFilter.newBuilder().setApplyLabelTransformer("false")))
             .build();
 
-    Assert.assertEquals(expectedFilter, actualProto);
-  }
-
-  @Test
-  public void passTest() {
-    RowFilter actualProto = FILTERS.pass().toProto();
-
-    RowFilter expectedFilter = RowFilter.newBuilder().setPassAllFilter(true).build();
-
-    Assert.assertEquals(expectedFilter, actualProto);
-  }
-
-  @Test
-  public void blockTest() {
-    RowFilter actualProto = FILTERS.block().toProto();
-
-    RowFilter expectedFilter = RowFilter.newBuilder().setBlockAllFilter(true).build();
-
-    Assert.assertEquals(expectedFilter, actualProto);
+    Assert.assertEquals(expectedFilter, actualFilter);
   }
 
   @Test
-  public void sinkTest() {
-    RowFilter actualProto = FILTERS.sink().toProto();
-
-    RowFilter expectedFilter = RowFilter.newBuilder().setSink(true).build();
-
-    Assert.assertEquals(expectedFilter, actualProto);
-  }
-
-  @Test
-  public void labelTest() {
-    RowFilter actualProto = FILTERS.label("my-label").toProto();
+  public void keyRegexTest() {
+    RowFilter actualFilter = FILTERS.key().regex(ByteString.copyFromUtf8(".*")).toProto();
 
-    RowFilter expectedFilter = RowFilter.newBuilder().setApplyLabelTransformer("my-label").build();
+    RowFilter expectedFilter =
+        RowFilter.newBuilder().setRowKeyRegexFilter(ByteString.copyFromUtf8(".*")).build();
 
-    Assert.assertEquals(expectedFilter, actualProto);
+    Assert.assertEquals(expectedFilter, actualFilter);
   }
 
   @Test
-  public void keyRegexTest() {
-    RowFilter actualProto = FILTERS.key().regex(".*").toProto();
+  public void keyRegexStringTest() {
+    RowFilter actualFilter = FILTERS.key().regex(".*").toProto();
 
     RowFilter expectedFilter =
         RowFilter.newBuilder().setRowKeyRegexFilter(ByteString.copyFromUtf8(".*")).build();
 
-    Assert.assertEquals(expectedFilter, actualProto);
+    Assert.assertEquals(expectedFilter, actualFilter);
   }
 
   @Test
   public void keyExactMatchTest() {
-    RowFilter actualProto = FILTERS.key().exactMatch(ByteString.copyFromUtf8(".*")).toProto();
+    RowFilter actualFilter = FILTERS.key().exactMatch(ByteString.copyFromUtf8(".*")).toProto();
 
     RowFilter expectedFilter =
         RowFilter.newBuilder().setRowKeyRegexFilter(ByteString.copyFromUtf8("\\.\\*")).build();
 
-    Assert.assertEquals(expectedFilter, actualProto);
+    Assert.assertEquals(expectedFilter, actualFilter);
   }
 
   @Test
   public void keySampleTest() {
-    RowFilter actualProto = FILTERS.key().sample(0.3).toProto();
+    RowFilter actualFilter = FILTERS.key().sample(0.3).toProto();
 
     RowFilter expectedFilter = RowFilter.newBuilder().setRowSampleFilter(0.3).build();
 
-    Assert.assertEquals(expectedFilter, actualProto);
+    Assert.assertEquals(expectedFilter, actualFilter);
   }
 
   @Test
   public void familyRegexTest() {
-    RowFilter actualProto = FILTERS.family().regex("^hi").toProto();
+    RowFilter actualFilter = FILTERS.family().regex("^hi").toProto();
 
     RowFilter expectedFilter = RowFilter.newBuilder().setFamilyNameRegexFilter("^hi").build();
 
-    Assert.assertEquals(expectedFilter, actualProto);
+    Assert.assertEquals(expectedFilter, actualFilter);
   }
 
   @Test
   public void familyExactMatchTest() {
-    RowFilter actualProto = FILTERS.family().exactMatch("^hi").toProto();
+    RowFilter actualFilter = FILTERS.family().exactMatch("^hi").toProto();
 
     RowFilter expectedFilter = RowFilter.newBuilder().setFamilyNameRegexFilter("\\^hi").build();
 
-    Assert.assertEquals(expectedFilter, actualProto);
+    Assert.assertEquals(expectedFilter, actualFilter);
   }
 
   @Test
   public void qualifierRegexTest() {
-    RowFilter actualProto = FILTERS.qualifier().regex("^hi").toProto();
+    RowFilter actualFilter = FILTERS.qualifier().regex(ByteString.copyFromUtf8("^hi")).toProto();
 
     RowFilter expectedFilter =
         RowFilter.newBuilder()
             .setColumnQualifierRegexFilter(ByteString.copyFromUtf8("^hi"))
             .build();
 
-    Assert.assertEquals(expectedFilter, actualProto);
+    Assert.assertEquals(expectedFilter, actualFilter);
+  }
+
+  @Test
+  public void qualifierRegexStringTest() {
+    RowFilter actualFilter = FILTERS.qualifier().regex("^hi").toProto();
+
+    RowFilter expectedFilter =
+        RowFilter.newBuilder()
+            .setColumnQualifierRegexFilter(ByteString.copyFromUtf8("^hi"))
+            .build();
+
+    Assert.assertEquals(expectedFilter, actualFilter);
   }
 
   @Test
   public void qualifierExactMatchTest() {
-    RowFilter actualProto = FILTERS.qualifier().exactMatch(ByteString.copyFromUtf8("^hi")).toProto();
+    RowFilter actualFilter =
+        FILTERS.qualifier().exactMatch(ByteString.copyFromUtf8("^hi")).toProto();
 
     RowFilter expectedFilter =
         RowFilter.newBuilder()
             .setColumnQualifierRegexFilter(ByteString.copyFromUtf8("\\^hi"))
             .build();
 
-    Assert.assertEquals(expectedFilter, actualProto);
+    Assert.assertEquals(expectedFilter, actualFilter);
   }
 
   @Test
-  public void qualifierRangeOpenClosed() {
-    String family = "family";
-    ByteString begin = ByteString.copyFromUtf8("begin");
-    ByteString end = ByteString.copyFromUtf8("end");
-    RowFilter actualProto = FILTERS.qualifier().rangeWithinFamily(family)
-        .startOpen(begin)
-        .endClosed(end)
-        .toProto();
+  public void qualifierRangeInFamilyClosedOpen() {
+    RowFilter actualFilter =
+        FILTERS
+            .qualifier()
+            .rangeWithinFamily("family")
+            .startClosed("begin")
+            .endOpen("end")
+            .toProto();
+
     RowFilter expectedFilter =
         RowFilter.newBuilder()
             .setColumnRangeFilter(
                 ColumnRange.newBuilder()
-                    .setFamilyName(family)
-                    .setStartQualifierOpen(begin)
-                    .setEndQualifierClosed(end))
+                    .setFamilyName("family")
+                    .setStartQualifierClosed(ByteString.copyFromUtf8("begin"))
+                    .setEndQualifierOpen(ByteString.copyFromUtf8("end")))
             .build();
 
-    Assert.assertEquals(expectedFilter, actualProto);
+    Assert.assertEquals(expectedFilter, actualFilter);
   }
 
   @Test
-  public void qualifierRangeClosedOpen() {
-    String family = "family";
-    ByteString begin = ByteString.copyFromUtf8("begin");
-    ByteString end = ByteString.copyFromUtf8("end");
-    RowFilter actualProto = FILTERS.qualifier().rangeWithinFamily(family)
-        .startClosed(begin)
-        .endOpen(end)
-        .toProto();
+  public void qualifierRangeInFamilyOpenClosed() {
+    RowFilter actualFilter =
+        FILTERS
+            .qualifier()
+            .rangeWithinFamily("family")
+            .startOpen("begin")
+            .endClosed("end")
+            .toProto();
+
     RowFilter expectedFilter =
         RowFilter.newBuilder()
             .setColumnRangeFilter(
                 ColumnRange.newBuilder()
-                    .setFamilyName(family)
-                    .setStartQualifierClosed(begin)
-                    .setEndQualifierOpen(end))
+                    .setFamilyName("family")
+                    .setStartQualifierOpen(ByteString.copyFromUtf8("begin"))
+                    .setEndQualifierClosed(ByteString.copyFromUtf8("end")))
             .build();
 
-    Assert.assertEquals(expectedFilter, actualProto);
+    Assert.assertEquals(expectedFilter, actualFilter);
+  }
+
+  @Test
+  public void qualifierRangeRange() {
+    RowFilter actualFilter =
+        FILTERS
+            .qualifier()
+            .rangeWithinFamily("family")
+            .startClosed("begin")
+            .endOpen("end")
+            .toProto();
+
+    RowFilter expectedFilter =
+        RowFilter.newBuilder()
+            .setColumnRangeFilter(
+                ColumnRange.newBuilder()
+                    .setFamilyName("family")
+                    .setStartQualifierClosed(ByteString.copyFromUtf8("begin"))
+                    .setEndQualifierOpen(ByteString.copyFromUtf8("end")))
+            .build();
+
+    Assert.assertEquals(expectedFilter, actualFilter);
+  }
+
+  @Test
+  public void timestampRange() {
+    RowFilter actualFilter =
+        FILTERS.timestamp().range().startClosed(1_000L).endOpen(30_000L).toProto();
+
+    RowFilter expectedFilter =
+        RowFilter.newBuilder()
+            .setTimestampRangeFilter(
+                TimestampRange.newBuilder()
+                    .setStartTimestampMicros(1_000L)
+                    .setEndTimestampMicros(30_000L))
+            .build();
+
+    Assert.assertEquals(expectedFilter, actualFilter);
+  }
+
+  @Test
+  public void timestampOpenClosedFakeRange() {
+    RowFilter actualFilter =
+        FILTERS.timestamp().range().startOpen(1_000L).endClosed(30_000L).toProto();
+
+    // open start & closed end are faked in the client by incrementing the query
+    RowFilter expectedFilter =
+        RowFilter.newBuilder()
+            .setTimestampRangeFilter(
+                TimestampRange.newBuilder()
+                    .setStartTimestampMicros(1_000L + 1)
+                    .setEndTimestampMicros(30_000L + 1))
+            .build();
+
+    Assert.assertEquals(expectedFilter, actualFilter);
   }
 
   @Test
   public void valueRegex() {
-    RowFilter actualProto = FILTERS.value().regex("some[0-9]regex").toProto();
+    RowFilter actualFilter = FILTERS.value().regex("some[0-9]regex").toProto();
 
     RowFilter expectedFilter =
         RowFilter.newBuilder()
             .setValueRegexFilter(ByteString.copyFromUtf8("some[0-9]regex"))
             .build();
 
-    Assert.assertEquals(expectedFilter, actualProto);
+    Assert.assertEquals(expectedFilter, actualFilter);
   }
 
   @Test
   public void valueExactMatch() {
-    RowFilter actualProto =
+    RowFilter actualFilter =
         FILTERS.value().exactMatch(ByteString.copyFromUtf8("some[0-9]regex")).toProto();
 
     RowFilter expectedFilter =
@@ -290,122 +329,108 @@ public class FiltersTest {
             .setValueRegexFilter(ByteString.copyFromUtf8("some\\[0\\-9\\]regex"))
             .build();
 
-    Assert.assertEquals(expectedFilter, actualProto);
+    Assert.assertEquals(expectedFilter, actualFilter);
   }
 
   @Test
   public void valueRangeClosedOpen() {
-    ByteString begin = ByteString.copyFromUtf8("begin");
-    ByteString end = ByteString.copyFromUtf8("end");
-
-    RowFilter actualProto = FILTERS.value().range()
-        .startOpen(begin)
-        .endClosed(end)
-        .toProto();
+    RowFilter actualFilter = FILTERS.value().range().startClosed("begin").endOpen("end").toProto();
 
     RowFilter expectedFilter =
         RowFilter.newBuilder()
             .setValueRangeFilter(
                 ValueRange.newBuilder()
-                    .setStartValueOpen(begin)
-                    .setEndValueClosed(end))
+                    .setStartValueClosed(ByteString.copyFromUtf8("begin"))
+                    .setEndValueOpen(ByteString.copyFromUtf8("end")))
             .build();
 
-    Assert.assertEquals(expectedFilter, actualProto);
+    Assert.assertEquals(expectedFilter, actualFilter);
   }
 
   @Test
   public void valueRangeOpenClosed() {
-    ByteString begin = ByteString.copyFromUtf8("begin");
-    ByteString end = ByteString.copyFromUtf8("end");
-
-    RowFilter actualProto = FILTERS.value().range()
-        .startClosed(begin)
-        .endOpen(end)
-        .toProto();
+    RowFilter actualFilter = FILTERS.value().range().startOpen("begin").endClosed("end").toProto();
 
     RowFilter expectedFilter =
         RowFilter.newBuilder()
             .setValueRangeFilter(
                 ValueRange.newBuilder()
-                    .setStartValueClosed(begin)
-                    .setEndValueOpen(end))
+                    .setStartValueOpen(ByteString.copyFromUtf8("begin"))
+                    .setEndValueClosed(ByteString.copyFromUtf8("end")))
             .build();
 
-    Assert.assertEquals(expectedFilter, actualProto);
+    Assert.assertEquals(expectedFilter, actualFilter);
   }
 
   @Test
   public void valueStripTest() {
-    RowFilter actualProto = FILTERS.value().strip().toProto();
+    RowFilter actualFilter = FILTERS.value().strip().toProto();
 
     RowFilter expectedFilter = RowFilter.newBuilder().setStripValueTransformer(true).build();
 
-    Assert.assertEquals(expectedFilter, actualProto);
+    Assert.assertEquals(expectedFilter, actualFilter);
   }
 
   @Test
   public void offsetCellsPerRowTest() {
-    RowFilter actualProto = FILTERS.offset().cellsPerRow(10).toProto();
+    RowFilter actualFilter = FILTERS.offset().cellsPerRow(10).toProto();
 
     RowFilter expectedFilter = RowFilter.newBuilder().setCellsPerRowOffsetFilter(10).build();
 
-    Assert.assertEquals(expectedFilter, actualProto);
+    Assert.assertEquals(expectedFilter, actualFilter);
   }
 
   @Test
   public void limitCellsPerRowTest() {
-    RowFilter actualProto = FILTERS.limit().cellsPerRow(10).toProto();
+    RowFilter actualFilter = FILTERS.limit().cellsPerRow(10).toProto();
 
     RowFilter expectedFilter = RowFilter.newBuilder().setCellsPerRowLimitFilter(10).build();
 
-    Assert.assertEquals(expectedFilter, actualProto);
+    Assert.assertEquals(expectedFilter, actualFilter);
   }
 
   @Test
   public void limitCellsPerColumnTest() {
-    RowFilter actualProto = FILTERS.limit().cellsPerColumn(10).toProto();
+    RowFilter actualFilter = FILTERS.limit().cellsPerColumn(10).toProto();
 
     RowFilter expectedFilter = RowFilter.newBuilder().setCellsPerColumnLimitFilter(10).build();
 
-    Assert.assertEquals(expectedFilter, actualProto);
+    Assert.assertEquals(expectedFilter, actualFilter);
   }
 
   @Test
-  public void timestampFullRangeTest() {
-    long start = TimeUnit.MILLISECONDS.toMicros(System.currentTimeMillis() - 10000000);
-    long end = TimeUnit.MILLISECONDS.toMicros(System.currentTimeMillis());
-    RowFilter actualProto = FILTERS.timestamp().range(start, end).toProto();
+  public void passTest() {
+    RowFilter actualFilter = FILTERS.pass().toProto();
 
-    RowFilter.Builder expected = RowFilter.newBuilder();
-    expected.getTimestampRangeFilterBuilder()
-      .setStartTimestampMicros(start)
-      .setEndTimestampMicros(end);
+    RowFilter expectedFilter = RowFilter.newBuilder().setPassAllFilter(true).build();
 
-    Assert.assertEquals(expected.build(), actualProto);
+    Assert.assertEquals(expectedFilter, actualFilter);
   }
 
   @Test
-  public void timestampStartTest() {
-    long start = TimeUnit.MILLISECONDS.toMicros(System.currentTimeMillis() - 10000000);
-    RowFilter actualProto = FILTERS.timestamp().range().startClosed(start).toProto();
+  public void blockTest() {
+    RowFilter actualFilter = FILTERS.block().toProto();
 
-    RowFilter.Builder expected = RowFilter.newBuilder();
-    expected.getTimestampRangeFilterBuilder()
-      .setStartTimestampMicros(start);
+    RowFilter expectedFilter = RowFilter.newBuilder().setBlockAllFilter(true).build();
 
-    Assert.assertEquals(expected.build(), actualProto);
+    Assert.assertEquals(expectedFilter, actualFilter);
   }
 
   @Test
-  public void timestampEndTest() {
-    long end = TimeUnit.MILLISECONDS.toMicros(System.currentTimeMillis());
-    RowFilter actualProto = FILTERS.timestamp().range().endOpen(end).toProto();
+  public void sinkTest() {
+    RowFilter actualFilter = FILTERS.sink().toProto();
+
+    RowFilter expectedFilter = RowFilter.newBuilder().setSink(true).build();
 
-    RowFilter.Builder expected = RowFilter.newBuilder();
-    expected.getTimestampRangeFilterBuilder()
-      .setEndTimestampMicros(end);
+    Assert.assertEquals(expectedFilter, actualFilter);
+  }
+
+  @Test
+  public void labelTest() {
+    RowFilter actualFilter = FILTERS.label("my-label").toProto();
+
+    RowFilter expectedFilter = RowFilter.newBuilder().setApplyLabelTransformer("my-label").build();
 
-    Assert.assertEquals(expected.build(), actualProto);
+    Assert.assertEquals(expectedFilter, actualFilter);
   }
 }
diff --git a/bigtable-client-core-parent/bigtable-client-core/src/test/java/com/google/cloud/bigtable/data/v2/wrappers/RangeTest.java b/bigtable-client-core-parent/bigtable-client-core/src/test/java/com/google/cloud/bigtable/data/v2/wrappers/RangeTest.java
new file mode 100644
index 000000000..4d400d086
--- /dev/null
+++ b/bigtable-client-core-parent/bigtable-client-core/src/test/java/com/google/cloud/bigtable/data/v2/wrappers/RangeTest.java
@@ -0,0 +1,198 @@
+/*
+ * Copyright 2018 Google LLC
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.cloud.bigtable.data.v2.wrappers;
+
+import com.google.cloud.bigtable.data.v2.wrappers.Range.BoundType;
+import com.google.cloud.bigtable.data.v2.wrappers.Range.ByteStringRange;
+import com.google.cloud.bigtable.data.v2.wrappers.Range.TimestampRange;
+import com.google.protobuf.ByteString;
+import org.junit.Assert;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+@RunWith(JUnit4.class)
+public class RangeTest {
+  @Test
+  public void timestampUnboundedTest() {
+    TimestampRange range = TimestampRange.unbounded();
+    Assert.assertEquals(BoundType.UNBOUNDED, range.getStartBound());
+    Assert.assertEquals(BoundType.UNBOUNDED, range.getEndBound());
+
+    Throwable actualError = null;
+    try {
+      //noinspection ResultOfMethodCallIgnored
+      range.getStart();
+    } catch (Throwable e) {
+      actualError = e;
+    }
+    Assert.assertTrue(actualError instanceof IllegalStateException);
+
+    try {
+      //noinspection ResultOfMethodCallIgnored
+      range.getEnd();
+    } catch (Throwable e) {
+      actualError = e;
+    }
+    Assert.assertTrue(actualError instanceof IllegalStateException);
+  }
+
+  @Test
+  public void timestampOfTest() {
+    TimestampRange range = TimestampRange.create(10, 2_000);
+    Assert.assertEquals(BoundType.CLOSED, range.getStartBound());
+    Assert.assertEquals(10, range.getStart().longValue());
+    Assert.assertEquals(BoundType.OPEN, range.getEndBound());
+    Assert.assertEquals(2_000, range.getEnd().longValue());
+  }
+
+  @Test
+  public void timestampChangeStartTest() {
+    TimestampRange range = TimestampRange.create(10, 2_000).startOpen(20L);
+
+    Assert.assertEquals(BoundType.OPEN, range.getEndBound());
+    Assert.assertEquals(2_000, range.getEnd().longValue());
+
+    Assert.assertEquals(BoundType.OPEN, range.getStartBound());
+    Assert.assertEquals(20, range.getStart().longValue());
+
+    range = range.startClosed(30L);
+    Assert.assertEquals(BoundType.CLOSED, range.getStartBound());
+    Assert.assertEquals(30, range.getStart().longValue());
+  }
+
+  @Test
+  public void timestampChangeEndTest() {
+    TimestampRange range = TimestampRange.create(10, 2_000).endClosed(1_000L);
+
+    Assert.assertEquals(BoundType.CLOSED, range.getStartBound());
+    Assert.assertEquals(10, range.getStart().longValue());
+
+    Assert.assertEquals(BoundType.CLOSED, range.getEndBound());
+    Assert.assertEquals(1_000, range.getEnd().longValue());
+
+    range = range.endOpen(3_000L);
+    Assert.assertEquals(BoundType.OPEN, range.getEndBound());
+    Assert.assertEquals(3_000, range.getEnd().longValue());
+  }
+
+  @Test
+  public void byteStringUnboundedTest() {
+    ByteStringRange range = ByteStringRange.unbounded();
+    Assert.assertEquals(BoundType.UNBOUNDED, range.getStartBound());
+    Assert.assertEquals(BoundType.UNBOUNDED, range.getEndBound());
+
+    Throwable actualError = null;
+    try {
+      range.getStart();
+    } catch (Throwable e) {
+      actualError = e;
+    }
+    Assert.assertTrue(actualError instanceof IllegalStateException);
+
+    try {
+      range.getEnd();
+    } catch (Throwable e) {
+      actualError = e;
+    }
+    Assert.assertTrue(actualError instanceof IllegalStateException);
+  }
+
+  @Test
+  public void byteStringOfTest() {
+    ByteStringRange range =
+        ByteStringRange.create(ByteString.copyFromUtf8("a"), ByteString.copyFromUtf8("b"));
+
+    Assert.assertEquals(BoundType.CLOSED, range.getStartBound());
+    Assert.assertEquals(ByteString.copyFromUtf8("a"), range.getStart());
+    Assert.assertEquals(BoundType.OPEN, range.getEndBound());
+    Assert.assertEquals(ByteString.copyFromUtf8("b"), range.getEnd());
+  }
+
+  @Test
+  public void byteStringOfStringTest() {
+    ByteStringRange range = ByteStringRange.create("a", "b");
+
+    Assert.assertEquals(BoundType.CLOSED, range.getStartBound());
+    Assert.assertEquals(ByteString.copyFromUtf8("a"), range.getStart());
+    Assert.assertEquals(BoundType.OPEN, range.getEndBound());
+    Assert.assertEquals(ByteString.copyFromUtf8("b"), range.getEnd());
+  }
+
+  @Test
+  public void byteStringChangeStartTest() {
+    ByteStringRange range =
+        ByteStringRange.create(ByteString.copyFromUtf8("a"), ByteString.copyFromUtf8("z"))
+            .startOpen(ByteString.copyFromUtf8("b"));
+
+    Assert.assertEquals(BoundType.OPEN, range.getEndBound());
+    Assert.assertEquals(ByteString.copyFromUtf8("z"), range.getEnd());
+
+    Assert.assertEquals(BoundType.OPEN, range.getStartBound());
+    Assert.assertEquals(ByteString.copyFromUtf8("b"), range.getStart());
+
+    range = range.startClosed(ByteString.copyFromUtf8("c"));
+    Assert.assertEquals(BoundType.CLOSED, range.getStartBound());
+    Assert.assertEquals(ByteString.copyFromUtf8("c"), range.getStart());
+  }
+
+  @Test
+  public void byteStringChangeStartStringTest() {
+    ByteStringRange range = ByteStringRange.create("a", "z").startOpen("b");
+
+    Assert.assertEquals(BoundType.OPEN, range.getEndBound());
+    Assert.assertEquals(ByteString.copyFromUtf8("z"), range.getEnd());
+
+    Assert.assertEquals(BoundType.OPEN, range.getStartBound());
+    Assert.assertEquals(ByteString.copyFromUtf8("b"), range.getStart());
+
+    range = range.startClosed("c");
+    Assert.assertEquals(BoundType.CLOSED, range.getStartBound());
+    Assert.assertEquals(ByteString.copyFromUtf8("c"), range.getStart());
+  }
+
+  @Test
+  public void byteStringChangeEndTest() {
+    ByteStringRange range =
+        ByteStringRange.create(ByteString.copyFromUtf8("a"), ByteString.copyFromUtf8("z"))
+            .endClosed(ByteString.copyFromUtf8("y"));
+
+    Assert.assertEquals(BoundType.CLOSED, range.getStartBound());
+    Assert.assertEquals(ByteString.copyFromUtf8("a"), range.getStart());
+
+    Assert.assertEquals(BoundType.CLOSED, range.getEndBound());
+    Assert.assertEquals(ByteString.copyFromUtf8("y"), range.getEnd());
+
+    range = range.endOpen(ByteString.copyFromUtf8("x"));
+    Assert.assertEquals(BoundType.OPEN, range.getEndBound());
+    Assert.assertEquals(ByteString.copyFromUtf8("x"), range.getEnd());
+  }
+
+  @Test
+  public void byteStringChangeEndStringTest() {
+    ByteStringRange range = ByteStringRange.create("a", "z").endClosed("y");
+
+    Assert.assertEquals(BoundType.CLOSED, range.getStartBound());
+    Assert.assertEquals(ByteString.copyFromUtf8("a"), range.getStart());
+
+    Assert.assertEquals(BoundType.CLOSED, range.getEndBound());
+    Assert.assertEquals(ByteString.copyFromUtf8("y"), range.getEnd());
+
+    range = range.endOpen("x");
+    Assert.assertEquals(BoundType.OPEN, range.getEndBound());
+    Assert.assertEquals(ByteString.copyFromUtf8("x"), range.getEnd());
+  }
+}
diff --git a/bigtable-hbase-2.x-parent/bigtable-hbase-2.x-integration-tests/src/test/java/com/google/cloud/bigtable/hbase/TestCheckAndMutateHBase2.java b/bigtable-hbase-2.x-parent/bigtable-hbase-2.x-integration-tests/src/test/java/com/google/cloud/bigtable/hbase/TestCheckAndMutateHBase2.java
index a5ac8147c..9fe9d46c8 100644
--- a/bigtable-hbase-2.x-parent/bigtable-hbase-2.x-integration-tests/src/test/java/com/google/cloud/bigtable/hbase/TestCheckAndMutateHBase2.java
+++ b/bigtable-hbase-2.x-parent/bigtable-hbase-2.x-integration-tests/src/test/java/com/google/cloud/bigtable/hbase/TestCheckAndMutateHBase2.java
@@ -26,6 +26,7 @@ import org.apache.hadoop.hbase.client.Put;
 import org.apache.hadoop.hbase.client.Result;
 import org.apache.hadoop.hbase.client.RowMutations;
 import org.apache.hadoop.hbase.client.Table;
+import org.apache.hadoop.hbase.filter.CompareFilter.CompareOp;
 import org.apache.hadoop.hbase.util.Bytes;
 import org.junit.Assert;
 import org.junit.Rule;
@@ -354,7 +355,11 @@ public class TestCheckAndMutateHBase2 extends AbstractTest {
     success = table.checkAndPut(rowKey, SharedTestEnvRule.COLUMN_FAMILY, qualToCheck,
       CompareOperator.NOT_EQUAL, Bytes.toBytes(4000l), someRandomPut);
     Assert.assertTrue("4000 != 2000 should succeed", success);
-  }
+
+    success = table.checkAndPut(rowKey, SharedTestEnvRule.COLUMN_FAMILY, qualToCheck,
+      CompareOperator.NOT_EQUAL, null, someRandomPut);
+    Assert.assertTrue("4000 != null should succeed", success);
+}
 
   @Test
   public void testCompareOperatorsVersions() throws IOException {
diff --git a/bigtable-hbase-2.x-parent/bigtable-hbase-2.x-integration-tests/src/test/java/com/google/cloud/bigtable/hbase/async/TestAsyncCheckAndMutate.java b/bigtable-hbase-2.x-parent/bigtable-hbase-2.x-integration-tests/src/test/java/com/google/cloud/bigtable/hbase/async/TestAsyncCheckAndMutate.java
index edf5f1ee9..cb5954601 100644
--- a/bigtable-hbase-2.x-parent/bigtable-hbase-2.x-integration-tests/src/test/java/com/google/cloud/bigtable/hbase/async/TestAsyncCheckAndMutate.java
+++ b/bigtable-hbase-2.x-parent/bigtable-hbase-2.x-integration-tests/src/test/java/com/google/cloud/bigtable/hbase/async/TestAsyncCheckAndMutate.java
@@ -39,6 +39,7 @@ import org.junit.rules.ExpectedException;
 import org.junit.runner.RunWith;
 import org.junit.runners.JUnit4;
 
+import java.util.Arrays;
 import java.util.List;
 import java.util.concurrent.ExecutionException;
 import java.util.concurrent.ExecutorService;
@@ -66,8 +67,6 @@ public class TestAsyncCheckAndMutate extends AbstractAsyncTest {
    * the same row.
    *
    * Requirement 7.3 - Pass a null value to check for the non-existence of a column.
-   * @throws ExecutionException 
-   * @throws InterruptedException 
    */
   @Test
   public void testCheckAndPutSameQual() throws Exception {
@@ -77,7 +76,7 @@ public class TestAsyncCheckAndMutate extends AbstractAsyncTest {
     byte[] qual = dataHelper.randomData("qualifier-");
     byte[] value1 = dataHelper.randomData("value-");
     byte[] value2 = dataHelper.randomData("value-");
-    
+
     // Put with a bad check on a null value, then try with a good one
     Put put = new Put(rowKey).addColumn(SharedTestEnvRule.COLUMN_FAMILY, qual, value1);
     boolean success = checkandMutate(table, rowKey, qual)
@@ -91,7 +90,7 @@ public class TestAsyncCheckAndMutate extends AbstractAsyncTest {
         .get();
 
     Assert.assertTrue(success);
-    
+
     // Fail on null check, now there's a value there
     put = new Put(rowKey).addColumn(SharedTestEnvRule.COLUMN_FAMILY, qual, value2);
     success = checkandMutate(table, rowKey, qual)
@@ -110,29 +109,27 @@ public class TestAsyncCheckAndMutate extends AbstractAsyncTest {
         .get();
 
     Assert.assertTrue(success);
-    
+
     // Check results
     Get get = new Get(rowKey);
     get.readVersions(5);
-    Result result = table.get(get).get();
+    Result result = getDefaultTable().get(get);
     Assert.assertEquals("Should be two results", 2, result.size());
     List<Cell> cells = result.getColumnCells(SharedTestEnvRule.COLUMN_FAMILY, qual);
     Assert.assertArrayEquals(value2, CellUtil.cloneValue(cells.get(0)));
     Assert.assertArrayEquals(value1, CellUtil.cloneValue(cells.get(1)));
   }
 
-  private CheckAndMutateBuilder checkandMutate(AsyncTable table, 
+  private CheckAndMutateBuilder checkandMutate(AsyncTable table,
       byte[] rowKey, byte[] qual) {
     return table.checkAndMutate(rowKey, SharedTestEnvRule.COLUMN_FAMILY).qualifier(qual);
   }
 
   /**
    * Further tests for requirements 7.1 and 7.3.
-   * @throws ExecutionException 
-   * @throws InterruptedException 
    */
   @Test
-  public void testcheckAndMutateSameQual() throws Exception {
+  public void testCheckAndMutateSameQual() throws Exception {
     // Initialize
     AsyncTable table = getDefaultAsyncTable(executor);
     byte[] rowKey = dataHelper.randomData("rowKey-");
@@ -156,7 +153,7 @@ public class TestAsyncCheckAndMutate extends AbstractAsyncTest {
 
     // Add a value and check again
     Put put = new Put(rowKey).addColumn(SharedTestEnvRule.COLUMN_FAMILY, qual, value1);
-    table.put(put);
+    getDefaultTable().put(put);
     success = table.checkAndMutate(rowKey, SharedTestEnvRule.COLUMN_FAMILY)
         .qualifier(qual)
         .ifEquals(value2)
@@ -169,13 +166,11 @@ public class TestAsyncCheckAndMutate extends AbstractAsyncTest {
         .thenDelete(delete)
         .get();
     Assert.assertTrue(success);
-    Assert.assertFalse("Row should be gone", table.exists(new Get(rowKey)).get());
+    Assert.assertFalse("Row should be gone", getDefaultTable().exists(new Get(rowKey)));
   }
 
   /**
    * Further tests for requirements 7.1 and 7.3.
-   * @throws ExecutionException 
-   * @throws InterruptedException 
    */
   @Test
   public void testCheckAndPutDiffQual() throws Exception {
@@ -226,22 +221,20 @@ public class TestAsyncCheckAndMutate extends AbstractAsyncTest {
     // Check results
     Get get = new Get(rowKey);
     get.readVersions(5);
-    Result result = table.get(get).get();
+    Result result = getDefaultTable().get(get);
     Assert.assertEquals("Should be two results", 2, result.size());
     Assert.assertArrayEquals(value1, CellUtil.cloneValue(result.getColumnLatestCell(SharedTestEnvRule.COLUMN_FAMILY, qual1)));
     Assert.assertArrayEquals(value2, CellUtil.cloneValue(result.getColumnLatestCell(SharedTestEnvRule.COLUMN_FAMILY,
       qual2)));
 
-    
+
   }
 
   /**
    * Further tests for requirements 7.1 and 7.3.
-   * @throws ExecutionException 
-   * @throws InterruptedException 
    */
   @Test
-  public void testcheckAndMutateDiffQual() throws Exception {
+  public void testCheckAndMutateDiffQual() throws Exception {
     // Initialize
     AsyncTable table = getDefaultAsyncTable(executor);
     byte[] rowKey = dataHelper.randomData("rowKey-");
@@ -269,7 +262,7 @@ public class TestAsyncCheckAndMutate extends AbstractAsyncTest {
     Put put = new Put(rowKey)
         .addColumn(SharedTestEnvRule.COLUMN_FAMILY, qual1, value1)
         .addColumn(SharedTestEnvRule.COLUMN_FAMILY, qual2, value2);
-    table.put(put);
+    getDefaultTable().put(put);
 
     // Fail on null check, now there's a value there
     success = table.checkAndMutate(rowKey, SharedTestEnvRule.COLUMN_FAMILY)
@@ -297,19 +290,15 @@ public class TestAsyncCheckAndMutate extends AbstractAsyncTest {
         .thenDelete(delete)
         .get();
     Assert.assertTrue(success);
-    Assert.assertFalse("Row should be gone", table.exists(new Get(rowKey)).get());
-
-    
+    Assert.assertFalse("Row should be gone", getDefaultTable().exists(new Get(rowKey)));
   }
 
   /**
    * Requirement 7.2 - Throws an IOException if the check is for a row other than the one in the
    * mutation attempt.
-   * @throws ExecutionException 
-   * @throws InterruptedException 
    */
   @Test
-  public void testCheckAndPutDiffRow() throws Exception {
+  public void testCheckAndPutDiffRow() throws Throwable {
     // Initialize
     AsyncTable table = getDefaultAsyncTable(executor);
     byte[] rowKey1 = dataHelper.randomData("rowKey-");
@@ -321,17 +310,20 @@ public class TestAsyncCheckAndMutate extends AbstractAsyncTest {
     Put put = new Put(rowKey1).addColumn(SharedTestEnvRule.COLUMN_FAMILY, qual, value);
     //Fix behavior as a part of Async Impl
     expectedException.expect(DoNotRetryIOException.class);
-//    expectedException.expect(IOException.class);
     expectedException.expectMessage("Action's getRow must match");
-    table.checkAndMutate(rowKey2, SharedTestEnvRule.COLUMN_FAMILY)
-        .qualifier(qual)
-        .ifNotExists()
-        .thenPut(put)
-        .get();
+    try {
+      table.checkAndMutate(rowKey2, SharedTestEnvRule.COLUMN_FAMILY)
+          .qualifier(qual)
+          .ifNotExists()
+          .thenPut(put)
+          .get();
+    } catch (ExecutionException e) {
+      throw e.getCause();
+    }
   }
 
   @Test
-  public void testcheckAndMutateDiffRow() throws Exception {
+  public void testCheckAndMutateDiffRow() throws Throwable {
     // Initialize
     AsyncTable table = getDefaultAsyncTable(executor);
     byte[] rowKey1 = dataHelper.randomData("rowKey-");
@@ -341,13 +333,16 @@ public class TestAsyncCheckAndMutate extends AbstractAsyncTest {
     // Put then again
     Delete delete = new Delete(rowKey1).addColumns(SharedTestEnvRule.COLUMN_FAMILY, qual);
     expectedException.expect(DoNotRetryIOException.class);
-//    expectedException.expect(IOException.class);
     expectedException.expectMessage("Action's getRow must match");
-    table.checkAndMutate(rowKey2, SharedTestEnvRule.COLUMN_FAMILY)
-        .ifEquals(qual)
-        .ifNotExists()
-        .thenDelete(delete)
-        .get();
+    try {
+      table.checkAndMutate(rowKey2, SharedTestEnvRule.COLUMN_FAMILY)
+          .ifEquals(qual)
+          .ifNotExists()
+          .thenDelete(delete)
+          .get();
+    } catch (ExecutionException e) {
+      throw e.getCause();
+    }
   }
 
   @Test
@@ -383,7 +378,7 @@ public class TestAsyncCheckAndMutate extends AbstractAsyncTest {
     Put put = new Put(rowKey)
         .addColumn(SharedTestEnvRule.COLUMN_FAMILY, qualCheck, valueCheck)
         .addColumn(SharedTestEnvRule.COLUMN_FAMILY, qualDelete, Bytes.toBytes("todelete"));
-    table.put(put);
+    getDefaultTable().put(put);
     // Fail on null check, now there's a value there
     success = table.checkAndMutate(rowKey, SharedTestEnvRule.COLUMN_FAMILY)
        .qualifier(qualCheck)
@@ -405,7 +400,7 @@ public class TestAsyncCheckAndMutate extends AbstractAsyncTest {
         .get();
     Assert.assertTrue(success);
 
-    Result row = table.get(new Get(rowKey).addFamily(SharedTestEnvRule.COLUMN_FAMILY)).get();
+    Result row = getDefaultTable().get(new Get(rowKey).addFamily(SharedTestEnvRule.COLUMN_FAMILY));
     // QualCheck and QualPut should exist
     Assert.assertEquals(2, row.size());
     Assert.assertFalse(
@@ -425,8 +420,8 @@ public class TestAsyncCheckAndMutate extends AbstractAsyncTest {
 
     AsyncTable table = getDefaultAsyncTable(executor);
 
-    table.put(new Put(rowKey).addColumn(SharedTestEnvRule.COLUMN_FAMILY, qualToCheck,
-      Bytes.toBytes(2000l)));
+    getDefaultTable().put(new Put(rowKey)
+      .addColumn(SharedTestEnvRule.COLUMN_FAMILY, qualToCheck, Bytes.toBytes(2000l)));
 
     Put someRandomPut =
         new Put(rowKey).addColumn(SharedTestEnvRule.COLUMN_FAMILY, otherQual, Bytes.toBytes(1l));
@@ -514,6 +509,13 @@ public class TestAsyncCheckAndMutate extends AbstractAsyncTest {
         .thenPut(someRandomPut)
         .get();
     Assert.assertTrue("4000 != 2000 should succeed", success);
+
+    success = table.checkAndMutate(rowKey, SharedTestEnvRule.COLUMN_FAMILY)
+        .qualifier(qualToCheck)
+        .ifMatches(CompareOperator.NOT_EQUAL, null)
+        .thenPut(someRandomPut)
+        .get();
+    Assert.assertTrue("4000 != null should succeed", success);
   }
 
   @Test
@@ -527,11 +529,13 @@ public class TestAsyncCheckAndMutate extends AbstractAsyncTest {
     Put someRandomPut =
         new Put(rowKey).addColumn(SharedTestEnvRule.COLUMN_FAMILY, otherQual, Bytes.toBytes(1l));
 
-    table.put(new Put(rowKey, System.currentTimeMillis() - 10000)
-        .addColumn(SharedTestEnvRule.COLUMN_FAMILY, qualToCheck, Bytes.toBytes(2000l)));
+    long now = System.currentTimeMillis();
+    Put put1 = new Put(rowKey, now - 10000).addColumn(SharedTestEnvRule.COLUMN_FAMILY, qualToCheck,
+      Bytes.toBytes(2000l));
+    Put put2 = new Put(rowKey, now).addColumn(SharedTestEnvRule.COLUMN_FAMILY, qualToCheck,
+      Bytes.toBytes(4000l));
 
-    table.put(new Put(rowKey, System.currentTimeMillis()).addColumn(SharedTestEnvRule.COLUMN_FAMILY,
-      qualToCheck, Bytes.toBytes(4000l)));
+    getDefaultTable().put(Arrays.asList(put1, put2));
 
     success = table.checkAndMutate(rowKey, SharedTestEnvRule.COLUMN_FAMILY)
         .qualifier(qualToCheck)
@@ -539,6 +543,5 @@ public class TestAsyncCheckAndMutate extends AbstractAsyncTest {
         .thenPut(someRandomPut)
         .get();
     Assert.assertFalse("3000 > 4000 should fail", success);
-
   }
 }
diff --git a/bigtable-hbase-parent/bigtable-hbase-integration-tests-common/src/main/java/com/google/cloud/bigtable/hbase/TestCheckAndMutate.java b/bigtable-hbase-parent/bigtable-hbase-integration-tests-common/src/main/java/com/google/cloud/bigtable/hbase/TestCheckAndMutate.java
index 2999528fb..3d48d814c 100644
--- a/bigtable-hbase-parent/bigtable-hbase-integration-tests-common/src/main/java/com/google/cloud/bigtable/hbase/TestCheckAndMutate.java
+++ b/bigtable-hbase-parent/bigtable-hbase-integration-tests-common/src/main/java/com/google/cloud/bigtable/hbase/TestCheckAndMutate.java
@@ -351,7 +351,11 @@ public class TestCheckAndMutate extends AbstractTest {
     success = table.checkAndPut(rowKey, SharedTestEnvRule.COLUMN_FAMILY, qualToCheck,
       CompareOp.NOT_EQUAL, Bytes.toBytes(4000l), someRandomPut);
     Assert.assertTrue("4000 != 2000 should succeed", success);
-  }
+
+    success = table.checkAndPut(rowKey, SharedTestEnvRule.COLUMN_FAMILY, qualToCheck,
+      CompareOp.NOT_EQUAL, null, someRandomPut);
+    Assert.assertTrue("4000 != null should succeed", success);
+}
 
   @Test
   public void testCompareOpsVersions() throws IOException {
@@ -373,6 +377,5 @@ public class TestCheckAndMutate extends AbstractTest {
     success = table.checkAndPut(rowKey, SharedTestEnvRule.COLUMN_FAMILY, qualToCheck,
       CompareOp.GREATER, Bytes.toBytes(3000l), someRandomPut);
     Assert.assertFalse("3000 > 4000 should fail", success);
-
   }
 }