Repository: GoogleCloudPlatform/cloud-bigtable-client
Pull request #1683 (2018-02-17 02:12:28)

Filter sync
Sync with the filters that are in the upcoming veneer client. The changes are functionally split between commits to ease review. I think #7 is the only controversial one. 

1. reorder classes to make it easier to see the rest of the changes
2. cosmetic changes (doesn't api)
  - javadoc updates
  - decorate args with @Nonnull & @Nullable
  - enforce nonnull with preconditions
  - fix Preconditions import
  - minor style changes
3. Adds a common range base class to make all ranges consistent and updates `QualifierRangeFilter`, `TimestampRangeFilter` & `ValueRangeFilter` to inherit from it.
4. Make all Filters cloneable instead of just ChainFilter
5. remove range(start,end) from TimestampRangeFilter in favor of range().of()
6. overwrites all of the FilterTests with ones from the veneer client
7. removes FamilyFilter#regex(ByteString)

Justification for removing FamilyFilter#regex(ByteString):
I think this is semantically wrong, family ids are strings and should be compared to strings directly.
Furthermore, family ids are a maximum of 64 characters and the optimization lowers the copies from 3 to 2., so I think the benefit is negligible.


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 bf97ad4e0..82c72264e 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
@@ -15,16 +15,14 @@ d * Copyright 2017 Google Inc. All Rights Reserved.
  */
 package com.google.cloud.bigtable.hbase2_x;
 
-import static com.google.cloud.bigtable.hbase2_x.FutureUtils.toCompletableFuture;
 import static java.util.stream.Collectors.toList;
 
 import java.io.Closeable;
 import java.io.IOException;
 import java.util.List;
 import java.util.concurrent.CompletableFuture;
-import java.util.concurrent.ExecutorService;
 import java.util.concurrent.TimeUnit;
-import java.util.stream.Stream;
+import java.util.function.Function;
 
 import org.apache.hadoop.conf.Configuration;
 import org.apache.hadoop.hbase.CompareOperator;
@@ -43,21 +41,16 @@ import org.apache.hadoop.hbase.client.RowMutations;
 import org.apache.hadoop.hbase.client.Scan;
 import org.apache.hadoop.hbase.client.ScanResultConsumer;
 import org.apache.hadoop.hbase.client.metrics.ScanMetrics;
-import org.apache.hadoop.hbase.filter.CompareFilter.CompareOp;
 import org.apache.hadoop.hbase.filter.FilterList;
 import org.apache.hadoop.hbase.filter.KeyOnlyFilter;
 import org.apache.hadoop.hbase.shaded.com.google.common.base.Preconditions;
 
 import com.google.bigtable.v2.CheckAndMutateRowRequest;
-import com.google.bigtable.v2.CheckAndMutateRowResponse;
 import com.google.bigtable.v2.MutateRowRequest;
-import com.google.bigtable.v2.MutateRowResponse;
-import com.google.bigtable.v2.Mutation;
 import com.google.bigtable.v2.ReadModifyWriteRowRequest;
 import com.google.bigtable.v2.ReadModifyWriteRowResponse;
 import com.google.cloud.bigtable.config.Logger;
 import com.google.bigtable.v2.ReadRowsRequest;
-import com.google.cloud.bigtable.grpc.BigtableDataClient;
 import com.google.cloud.bigtable.grpc.BigtableSession;
 import com.google.cloud.bigtable.grpc.scanner.FlatRow;
 import com.google.cloud.bigtable.hbase.AbstractBigtableTable;
@@ -65,7 +58,6 @@ import com.google.cloud.bigtable.hbase.BatchExecutor;
 import com.google.cloud.bigtable.hbase.adapters.Adapters;
 import com.google.cloud.bigtable.hbase.adapters.CheckAndMutateUtil;
 import com.google.cloud.bigtable.hbase.adapters.HBaseRequestAdapter;
-import com.google.common.util.concurrent.ListenableFuture;
 
 import io.grpc.stub.StreamObserver;
 import io.opencensus.trace.Span;
@@ -77,27 +69,28 @@ import io.opencensus.trace.Tracing;
  * 
  * @author spollapally
  */
-@SuppressWarnings("deprecation")
 public class BigtableAsyncTable implements AsyncTable {
 
-  protected static final Logger LOG = new Logger(AbstractBigtableTable.class);
+  private static final Logger LOG = new Logger(AbstractBigtableTable.class);
   private static final Tracer TRACER = Tracing.getTracer();
 
+  private static <T, R> List<R> map(List<T> list, Function<T, R> f) {
+    return list.stream().map(f).collect(toList());
+  }
+
   private final BigtableAsyncConnection asyncConnection;
   private final BigtableDataClient client;
   private final HBaseRequestAdapter hbaseAdapter;
   private final TableName tableName;
-  private final ExecutorService executorService;
   private BatchExecutor batchExecutor;
 
   public BigtableAsyncTable(BigtableAsyncConnection asyncConnection,
-      HBaseRequestAdapter hbaseAdapter, ExecutorService executorService) {
+      HBaseRequestAdapter hbaseAdapter) {
     this.asyncConnection = asyncConnection;
     BigtableSession session = asyncConnection.getSession();
-    this.client = session.getDataClient();
+    this.client = new BigtableDataClient(session.getDataClient());
     this.hbaseAdapter = hbaseAdapter;
     this.tableName = hbaseAdapter.getTableName();
-    this.executorService = executorService;
   }
 
   protected synchronized BatchExecutor getBatchExecutor() {
@@ -110,25 +103,23 @@ public class BigtableAsyncTable implements AsyncTable {
   @Override
   public CompletableFuture<Result> append(Append append) {
     ReadModifyWriteRowRequest request = hbaseAdapter.adapt(append);
-    ListenableFuture<ReadModifyWriteRowResponse> future = client.readModifyWriteRowAsync(request);
-
-    return FutureUtils.toCompletableFuture(future)
-        .thenApply(response -> append.isReturnResults() ?
-            Adapters.ROW_ADAPTER.adaptResponse(response.getRow()) :
-            null
-        );
+    Function<? super ReadModifyWriteRowResponse, ? extends Result> adaptRowFunction = response -> 
+        append.isReturnResults()
+            ? Adapters.ROW_ADAPTER.adaptResponse(response.getRow())
+            : null;
+    return client.readModifyWriteRowAsync(request).thenApply(adaptRowFunction);
   }
 
   @SuppressWarnings("unchecked")
   @Override
   public <T> List<CompletableFuture<T>> batch(List<? extends Row> actions) {
-    List<? extends Row> updatedActions = actions.stream()
-        .map(row -> row instanceof Get ? fromHB2Get((Get) row) : row)
-        .collect(toList());
+    List<? extends Row> updatedActions =
+        map(actions, row -> 
+            row instanceof Get 
+              ? fromHB2Get((Get) row)
+              : row);
     // TODO: The CompletableFutures need to return Void for Put/Delete.
-    return asyncRequests(updatedActions).stream()
-        .map(lfuture -> (CompletableFuture<T>) toCompletableFuture(lfuture))
-        .collect(toList());
+    return map(asyncRequests(updatedActions), f -> (CompletableFuture<T>) f);
   }
 
   @Override
@@ -184,31 +175,25 @@ public class BigtableAsyncTable implements AsyncTable {
     public CompletableFuture<Boolean> thenPut(Put put) {
       preCheck();
       try {
-        return call(put.getRow(), hbaseAdapter.adapt(put).getMutationsList());
+        return call(put.getRow(), hbaseAdapter.adapt(put));
       } catch (Exception e) {
         return FutureUtils.failedFuture(e);
       }
     }
 
-    private CheckAndMutateRowRequest getRequest(CompareOp compareOp, byte[] actionRow,
-        List<Mutation> mutations) throws IOException {
-      return CheckAndMutateUtil.makeConditionalMutationRequest(
-          hbaseAdapter,
-          row,
-          family,
-          qualifier,
-          compareOp,
-          value,
-          actionRow,
-          mutations);
-    }
-
-    private CompletableFuture<Boolean> call(byte[] row,
-        List<Mutation> mutations) throws IOException {
-      CompareOp compareOp = BigtableTable.toCompareOp(op);
-      CheckAndMutateRowRequest request = getRequest(compareOp, row, mutations);
-      ListenableFuture<CheckAndMutateRowResponse> lfuture = client.checkAndMutateRowAsync(request);
-      return FutureUtils.toCompletableFuture(lfuture).thenApply(
+    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));
     }
 
@@ -216,7 +201,7 @@ public class BigtableAsyncTable implements AsyncTable {
     public CompletableFuture<Boolean> thenDelete(Delete delete) {
       preCheck();
       try {
-        return call(delete.getRow(), hbaseAdapter.adapt(delete).getMutationsList());
+        return call(delete.getRow(), hbaseAdapter.adapt(delete));
       } catch (Exception e) {
         return FutureUtils.failedFuture(e);
       }
@@ -226,51 +211,40 @@ public class BigtableAsyncTable implements AsyncTable {
     public CompletableFuture<Boolean> thenMutate(RowMutations mutation) {
       preCheck();
       try {
-        return call(mutation.getRow(), hbaseAdapter.adapt(mutation).getMutationsList());
+        return call(mutation.getRow(), hbaseAdapter.adapt(mutation));
       } catch (Exception e) {
         return FutureUtils.failedFuture(e);
       }
-
     }
   }
 
   @Override
   public CompletableFuture<Void> delete(Delete delete) {
     // figure out how to time this with Opencensus
-    MutateRowRequest request = hbaseAdapter.adapt(delete);
-    ListenableFuture<MutateRowResponse> future = client.mutateRowAsync(request);
-    return toCompletableFuture(future, executorService)
+    return client.mutateRowAsync(hbaseAdapter.adapt(delete))
         .thenApply(r -> null);
   }
 
   @Override
   public List<CompletableFuture<Void>> delete(List<Delete> deletes) {
-    Stream<CompletableFuture<Void>> stream = asyncRequests(deletes)
-          .stream()
-          .map(lfuture -> toCompletableFuture(lfuture)
-                .thenApply(r -> null));
-    return stream.collect(toList());
+    return map(asyncRequests(deletes), cf -> cf.thenApply(r -> null));
   }
 
-  private List<ListenableFuture<?>> asyncRequests(List<? extends Row> actions) {
-    Object[] results = new Object[actions.size()];
-    return getBatchExecutor().issueAsyncRowRequests(actions, results, null);
+  private <T> List<CompletableFuture<?>> asyncRequests(List<? extends Row> actions) {
+    return map(getBatchExecutor().issueAsyncRowRequests(actions, new Object[actions.size()], null),
+      FutureUtils::toCompletableFuture);
   }
 
   @Override
   public CompletableFuture<Result> get(Get get) {
     ReadRowsRequest request = hbaseAdapter.adapt(fromHB2Get(get));
-    ListenableFuture<List<FlatRow>> future = client.readFlatRowsAsync(request);
-    return toCompletableFuture(future, (list -> toResult("get", list)),
-      executorService);
+    return client.readFlatRowsAsync(request).thenApply(BigtableAsyncTable::toResult);
   }
   
   private static Get fromHB2Get(Get get) {
-    if (get.isCheckExistenceOnly()) {
-      return addKeyOnlyFilter(get);
-    } else {
-      return get;
-    }
+    return get.isCheckExistenceOnly()
+      ? addKeyOnlyFilter(get)
+      : get;
   }
 
   private static Get addKeyOnlyFilter(Get get) {
@@ -288,40 +262,33 @@ public class BigtableAsyncTable implements AsyncTable {
     return get(addKeyOnlyFilter(get)).thenApply(r -> !r.isEmpty());
   }
 
-  private static Result toResult(String method, List<FlatRow> list) {
-    return Adapters.FLAT_ROW_ADAPTER.adaptResponse(getSingleResult(method, list));
+  private static Result toResult(List<FlatRow> list) {
+    return Adapters.FLAT_ROW_ADAPTER.adaptResponse(getSingleResult(list));
   }
 
-  private static FlatRow getSingleResult(String method, List<FlatRow> list) {
+  private static FlatRow getSingleResult(List<FlatRow> list) {
     switch (list.size()) {
     case 0:
       return null;
     case 1:
       return list.get(0);
     default:
-      throw new IllegalStateException("Multiple responses found for " + method);
+      throw new IllegalStateException("Multiple responses found for Get");
     }
   }
 
   @SuppressWarnings("unchecked")
   @Override
   public List<CompletableFuture<Result>> get(List<Get> gets) {
-    List<Get> hb1Gets = gets.stream()
-        .map(BigtableAsyncTable::fromHB2Get)
-        .collect(toList());
-    return asyncRequests(hb1Gets).stream()
-        .map(lfuture ->  (CompletableFuture<Result>) toCompletableFuture(lfuture))
-        .collect(toList());
+    List<Get> hb1Gets = map(gets, BigtableAsyncTable::fromHB2Get);
+    return map(asyncRequests(hb1Gets), 
+      (f -> (CompletableFuture<Result>) f));
   }
 
   @Override
   public List<CompletableFuture<Boolean>> exists(List<Get> gets) {
-    List<Get> existGets = gets.stream()
-          .map(BigtableAsyncTable::addKeyOnlyFilter)
-          .collect(toList());
-    return get(existGets).stream()
-          .map(cf -> cf.thenApply(r -> !r.isEmpty()))
-          .collect(toList());
+    List<Get> existGets = map(gets, BigtableAsyncTable::addKeyOnlyFilter);
+    return map(get(existGets), cf -> cf.thenApply(r -> !r.isEmpty()));
   }
 
   @Override
@@ -361,34 +328,26 @@ public class BigtableAsyncTable implements AsyncTable {
 
   @Override
   public CompletableFuture<Result> increment(Increment increment) {
-    ReadModifyWriteRowRequest request = hbaseAdapter.adapt(increment);
-    return FutureUtils.toCompletableFuture(client.readModifyWriteRowAsync(request))
-        .thenApply( response -> Adapters.ROW_ADAPTER.adaptResponse(response.getRow()));
+    return client.readModifyWriteRowAsync(hbaseAdapter.adapt(increment))
+        .thenApply(response -> Adapters.ROW_ADAPTER.adaptResponse(response.getRow()));
   }
 
   @Override
   public CompletableFuture<Void> mutateRow(RowMutations rowMutations) {
-    MutateRowRequest request = hbaseAdapter.adapt(rowMutations);
-    ListenableFuture<MutateRowResponse> future = client.mutateRowAsync(request);
-    return toCompletableFuture(future, executorService)
+    return client.mutateRowAsync(hbaseAdapter.adapt(rowMutations))
         .thenApply(r -> null);
   }
 
   @Override
   public CompletableFuture<Void> put(Put put) {
     // figure out how to time this with Opencensus
-    MutateRowRequest request = hbaseAdapter.adapt(put);
-    ListenableFuture<?> future = client.mutateRowAsync(request);
-    return toCompletableFuture(future, executorService).thenApply(r -> null);
+    return client.mutateRowAsync(hbaseAdapter.adapt(put))
+        .thenApply(r -> null);
   }
 
   @Override
   public List<CompletableFuture<Void>> put(List<Put> puts) {
-    Stream<CompletableFuture<Void>> stream = asyncRequests(puts)
-        .stream()
-        .map(lfuture -> toCompletableFuture(lfuture)
-              .thenApply(r -> null));
-    return stream.collect(toList());
+    return map(asyncRequests(puts), f -> f.thenApply(r -> null));
   }
 
   @Override
@@ -397,11 +356,8 @@ public class BigtableAsyncTable implements AsyncTable {
       throw new UnsupportedOperationException(
           "scanAll with while match filter is not allowed");
     }
-    return toCompletableFuture(client.readFlatRowsAsync(hbaseAdapter.adapt(scan)))
-         .thenApply(list -> 
-             list.stream()
-                 .map(row -> Adapters.FLAT_ROW_ADAPTER.adaptResponse(row))
-                 .collect(toList()));
+    return client.readFlatRowsAsync(hbaseAdapter.adapt(scan))
+         .thenApply(list -> map(list, Adapters.FLAT_ROW_ADAPTER::adaptResponse));
   }
 
   /** {@inheritDoc} */
@@ -411,7 +367,7 @@ public class BigtableAsyncTable implements AsyncTable {
     Span span = TRACER.spanBuilder("BigtableTable.scan").startSpan();
     try (Closeable c = TRACER.withSpan(span)) {
       com.google.cloud.bigtable.grpc.scanner.ResultScanner<FlatRow> scanner =
-          client.readFlatRows(hbaseAdapter.adapt(scan));
+          client.getClient().readFlatRows(hbaseAdapter.adapt(scan));
       if (AbstractBigtableTable.hasWhileMatchFilter(scan.getFilter())) {
         return Adapters.BIGTABLE_WHILE_MATCH_RESULT_RESULT_SCAN_ADAPTER.adapt(scanner, span);
       }
@@ -445,7 +401,7 @@ public class BigtableAsyncTable implements AsyncTable {
       throw new UnsupportedOperationException(
           "scan with consumer and while match filter is not allowed");
     }
-    client.readFlatRows(hbaseAdapter.adapt(scan), new StreamObserver<FlatRow>() {
+    client.getClient().readFlatRows(hbaseAdapter.adapt(scan), new StreamObserver<FlatRow>() {
       @Override
       public void onNext(FlatRow value) {
         consumer.onNext(Adapters.FLAT_ROW_ADAPTER.adaptResponse(value));
diff --git a/bigtable-hbase-2.x-parent/bigtable-hbase-2.x/src/main/java/com/google/cloud/bigtable/hbase2_x/BigtableDataClient.java b/bigtable-hbase-2.x-parent/bigtable-hbase-2.x/src/main/java/com/google/cloud/bigtable/hbase2_x/BigtableDataClient.java
new file mode 100644
index 000000000..d2db736c4
--- /dev/null
+++ b/bigtable-hbase-2.x-parent/bigtable-hbase-2.x/src/main/java/com/google/cloud/bigtable/hbase2_x/BigtableDataClient.java
@@ -0,0 +1,109 @@
+/*
+ * Copyright 2018 Google Inc. All Rights Reserved.
+ *
+ * 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
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.cloud.bigtable.hbase2_x;
+
+import static com.google.cloud.bigtable.hbase2_x.FutureUtils.toCompletableFuture;
+
+import java.util.List;
+import java.util.concurrent.CompletableFuture;
+
+import com.google.bigtable.v2.CheckAndMutateRowRequest;
+import com.google.bigtable.v2.CheckAndMutateRowResponse;
+import com.google.bigtable.v2.MutateRowRequest;
+import com.google.bigtable.v2.MutateRowResponse;
+import com.google.bigtable.v2.ReadModifyWriteRowRequest;
+import com.google.bigtable.v2.ReadModifyWriteRowResponse;
+import com.google.bigtable.v2.ReadRowsRequest;
+import com.google.bigtable.v2.SampleRowKeysRequest;
+import com.google.bigtable.v2.SampleRowKeysResponse;
+import com.google.cloud.bigtable.grpc.scanner.FlatRow;
+
+/**
+ * Interface to access v2 Bigtable data service methods.
+ *
+ * @author sduskis
+ * @version $Id: $Id
+ */
+public class BigtableDataClient {
+
+  private com.google.cloud.bigtable.grpc.BigtableDataClient dataClient;
+
+  public BigtableDataClient(com.google.cloud.bigtable.grpc.BigtableDataClient dataClient) {
+    this.dataClient = dataClient;
+  }
+
+  /**
+   * Mutate a row atomically.
+   *
+   * @return a {@link com.google.common.util.concurrent.ListenableFuture} that will finish when
+   * the mutation has completed.
+   * @param request a {@link com.google.bigtable.v2.MutateRowRequest} object.
+   */
+  public CompletableFuture<MutateRowResponse> mutateRowAsync(MutateRowRequest request) {
+    return toCompletableFuture(dataClient.mutateRowAsync(request));
+  }
+
+  /**
+   * Mutate a row atomically dependent on a precondition.
+   *
+   * @return a {@link com.google.common.util.concurrent.ListenableFuture} that will finish when
+   * the mutation has completed.
+   * @param request a {@link com.google.bigtable.v2.CheckAndMutateRowRequest} object.
+   */
+  public CompletableFuture<CheckAndMutateRowResponse> checkAndMutateRowAsync(
+      CheckAndMutateRowRequest request){
+    return toCompletableFuture(dataClient.checkAndMutateRowAsync(request));
+  }
+
+  /**
+   * Perform an atomic read-modify-write operation on a row,
+   *
+   * @return a {@link com.google.common.util.concurrent.ListenableFuture} that will finish when
+   * the mutation has completed.
+   * @param request a {@link com.google.bigtable.v2.ReadModifyWriteRowRequest} object.
+   */
+  public CompletableFuture<ReadModifyWriteRowResponse>
+      readModifyWriteRowAsync(ReadModifyWriteRowRequest request){
+    return toCompletableFuture(dataClient.readModifyWriteRowAsync(request));
+  }
+
+  /**
+   * Sample row keys from a table, returning a Future that will complete when the sampling has
+   * completed.
+   *
+   * @param request a {@link com.google.bigtable.v2.SampleRowKeysRequest} object.
+   * @return a {@link com.google.common.util.concurrent.ListenableFuture} object.
+   */
+  public CompletableFuture<List<SampleRowKeysResponse>>
+      sampleRowKeysAsync(SampleRowKeysRequest request) {
+    return toCompletableFuture(dataClient.sampleRowKeysAsync(request));
+  }
+
+  /**
+   * Read multiple {@link FlatRow}s into an in-memory list, in key order.
+   *
+   * @return a {@link com.google.common.util.concurrent.ListenableFuture} that will finish when
+   * all reads have completed.
+   * @param request a {@link com.google.bigtable.v2.ReadRowsRequest} object.
+   */
+  public CompletableFuture<List<FlatRow>> readFlatRowsAsync(ReadRowsRequest request) {
+    return toCompletableFuture(dataClient.readFlatRowsAsync(request));
+  }
+
+  public com.google.cloud.bigtable.grpc.BigtableDataClient getClient() {
+    return dataClient;
+  }
+}
diff --git a/bigtable-hbase-2.x-parent/bigtable-hbase-2.x/src/main/java/com/google/cloud/bigtable/hbase2_x/FutureUtils.java b/bigtable-hbase-2.x-parent/bigtable-hbase-2.x/src/main/java/com/google/cloud/bigtable/hbase2_x/FutureUtils.java
index dfb0cdcfb..62621abc0 100644
--- a/bigtable-hbase-2.x-parent/bigtable-hbase-2.x/src/main/java/com/google/cloud/bigtable/hbase2_x/FutureUtils.java
+++ b/bigtable-hbase-2.x-parent/bigtable-hbase-2.x/src/main/java/com/google/cloud/bigtable/hbase2_x/FutureUtils.java
@@ -17,7 +17,6 @@ package com.google.cloud.bigtable.hbase2_x;
 
 import java.util.concurrent.CompletableFuture;
 import java.util.concurrent.ExecutorService;
-import java.util.function.Function;
 
 import com.google.cloud.bigtable.config.Logger;
 import com.google.common.util.concurrent.FutureCallback;
@@ -38,18 +37,7 @@ public class FutureUtils {
   static Logger logger = new Logger(FutureUtils.class);
 
   public static <T> CompletableFuture<T> toCompletableFuture(ListenableFuture<T> listenableFuture) {
-    return toCompletableFuture(listenableFuture, DIRECT_EXECUTOR);
-  }
-
-  public static <T> CompletableFuture<T> toCompletableFuture(
-      final ListenableFuture<T> listenableFuture, ExecutorService es) {
-    return toCompletableFuture(listenableFuture, (r -> r), es);
-  }
-
-  public static <BTType, HBType> CompletableFuture<HBType> toCompletableFuture(
-      final ListenableFuture<BTType> listenableFuture, final Function<BTType, HBType> converter,
-      ExecutorService es) {
-    CompletableFuture<HBType> completableFuture = new CompletableFuture<HBType>() {
+    CompletableFuture<T> completableFuture = new CompletableFuture<T>() {
       @Override
       public boolean cancel(boolean mayInterruptIfRunning) {
         boolean result = listenableFuture.cancel(mayInterruptIfRunning);
@@ -58,20 +46,16 @@ public class FutureUtils {
       }
     };
 
-    FutureCallback<BTType> callback = new FutureCallback<BTType>() {
+    FutureCallback<T> callback = new FutureCallback<T>() {
       public void onFailure(Throwable throwable) {
         completableFuture.completeExceptionally(throwable);
       }
 
-      public void onSuccess(BTType s) {
-        try {
-          completableFuture.complete(converter.apply(s));
-        } catch (RuntimeException e) {
-          onFailure(e);
-        }
+      public void onSuccess(T t) {
+        completableFuture.complete(t);
       }
     };
-    Futures.addCallback(listenableFuture, callback, es);
+    Futures.addCallback(listenableFuture, callback);
 
     return completableFuture;
   }
diff --git a/bigtable-hbase-2.x-parent/bigtable-hbase-2.x/src/main/java/org/apache/hadoop/hbase/client/BigtableAsyncConnection.java b/bigtable-hbase-2.x-parent/bigtable-hbase-2.x/src/main/java/org/apache/hadoop/hbase/client/BigtableAsyncConnection.java
index 91b180b5e..5130bf354 100644
--- a/bigtable-hbase-2.x-parent/bigtable-hbase-2.x/src/main/java/org/apache/hadoop/hbase/client/BigtableAsyncConnection.java
+++ b/bigtable-hbase-2.x-parent/bigtable-hbase-2.x/src/main/java/org/apache/hadoop/hbase/client/BigtableAsyncConnection.java
@@ -269,12 +269,11 @@ public class BigtableAsyncConnection implements AsyncConnection, Closeable {
 
   @Override
   public AsyncTableBuilder<AsyncTable> getTableBuilder(TableName tableName,
-      final ExecutorService executorService) {
+      final ExecutorService ignored) {
     return new AsyncTableBuilder<AsyncTable>() {
       @Override
       public AsyncTable build() {
-        return new BigtableAsyncTable(BigtableAsyncConnection.this, createAdapter(tableName),
-            executorService);
+        return new BigtableAsyncTable(BigtableAsyncConnection.this, createAdapter(tableName));
       }
 
       @Override
diff --git a/bigtable-hbase-parent/bigtable-hbase/src/main/java/com/google/cloud/bigtable/hbase/adapters/CheckAndMutateUtil.java b/bigtable-hbase-parent/bigtable-hbase/src/main/java/com/google/cloud/bigtable/hbase/adapters/CheckAndMutateUtil.java
index 93603fbb8..322aa8d25 100644
--- a/bigtable-hbase-parent/bigtable-hbase/src/main/java/com/google/cloud/bigtable/hbase/adapters/CheckAndMutateUtil.java
+++ b/bigtable-hbase-parent/bigtable-hbase/src/main/java/com/google/cloud/bigtable/hbase/adapters/CheckAndMutateUtil.java
@@ -56,7 +56,7 @@ public class CheckAndMutateUtil {
   /**
    * <p>wasMutationApplied.</p>
    *
-   * @param requestBuilder a {@link com.google.bigtable.v2.CheckAndMutateRowRequest.Builder} object.
+   * @param request a {@link com.google.bigtable.v2.CheckAndMutateRowRequest} object.
    * @param response a {@link com.google.bigtable.v2.CheckAndMutateRowResponse} object.
    * @return a boolean.
    */
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-client-core-parent/bigtable-client-core/src/test/resources/com/google/cloud/bigtable/grpc/scanner/read-rows-acceptance-test.json b/bigtable-client-core-parent/bigtable-client-core/src/test/resources/com/google/cloud/bigtable/grpc/scanner/read-rows-acceptance-test.json
index 8d10da59c..1aa0a41b3 100644
--- a/bigtable-client-core-parent/bigtable-client-core/src/test/resources/com/google/cloud/bigtable/grpc/scanner/read-rows-acceptance-test.json
+++ b/bigtable-client-core-parent/bigtable-client-core/src/test/resources/com/google/cloud/bigtable/grpc/scanner/read-rows-acceptance-test.json
@@ -258,7 +258,7 @@
     {
       "name": "split cell, bare commit",
       "chunks": [
-        "row_key: \"RK\"\nfamily_name: \u003c\n  value: \"A\"\n\u003e\nqualifier: \u003c\n  value: \"C\"\n\u003e\ntimestamp_micros: 100\nvalue: \"v\"\nvalue_size: 10\ncommit_row: false\n",
+        "row_key: \"RK\"\nfamily_name: \u003c\n  value: \"A\"\n\u003e\nqualifier: \u003c\n  value: \"C\"\n\u003e\ntimestamp_micros: 100\nvalue: \"v\"\nvalue_size: 9\ncommit_row: false\n",
         "value: \"alue-VAL\"\ncommit_row: false\n",
         "commit_row: true\n"
       ],
@@ -286,7 +286,7 @@
     {
       "name": "split cell",
       "chunks": [
-        "row_key: \"RK\"\nfamily_name: \u003c\n  value: \"A\"\n\u003e\nqualifier: \u003c\n  value: \"C\"\n\u003e\ntimestamp_micros: 100\nvalue: \"v\"\nvalue_size: 10\ncommit_row: false\n",
+        "row_key: \"RK\"\nfamily_name: \u003c\n  value: \"A\"\n\u003e\nqualifier: \u003c\n  value: \"C\"\n\u003e\ntimestamp_micros: 100\nvalue: \"v\"\nvalue_size: 9\ncommit_row: false\n",
         "value: \"alue-VAL\"\ncommit_row: true\n"
       ],
       "results": [
@@ -304,9 +304,9 @@
     {
       "name": "split four ways",
       "chunks": [
-        "row_key: \"RK\"\nfamily_name: \u003c\n  value: \"A\"\n\u003e\nqualifier: \u003c\n  value: \"C\"\n\u003e\ntimestamp_micros: 100\nlabels: \"L\"\nvalue: \"v\"\nvalue_size: 10\ncommit_row: false\n",
-        "value: \"a\"\nvalue_size: 10\ncommit_row: false\n",
-        "value: \"l\"\nvalue_size: 10\ncommit_row: false\n",
+        "row_key: \"RK\"\nfamily_name: \u003c\n  value: \"A\"\n\u003e\nqualifier: \u003c\n  value: \"C\"\n\u003e\ntimestamp_micros: 100\nlabels: \"L\"\nvalue: \"v\"\nvalue_size: 9\ncommit_row: false\n",
+        "value: \"a\"\nvalue_size: 9\ncommit_row: false\n",
+        "value: \"l\"\nvalue_size: 9\ncommit_row: false\n",
         "value: \"ue-VAL\"\ncommit_row: true\n"
       ],
       "results": [
@@ -324,9 +324,9 @@
     {
       "name": "two split cells",
       "chunks": [
-        "row_key: \"RK\"\nfamily_name: \u003c\n  value: \"A\"\n\u003e\nqualifier: \u003c\n  value: \"C\"\n\u003e\ntimestamp_micros: 99\nvalue: \"v\"\nvalue_size: 10\ncommit_row: false\n",
+        "row_key: \"RK\"\nfamily_name: \u003c\n  value: \"A\"\n\u003e\nqualifier: \u003c\n  value: \"C\"\n\u003e\ntimestamp_micros: 99\nvalue: \"v\"\nvalue_size: 11\ncommit_row: false\n",
         "value: \"alue-VAL_1\"\ncommit_row: false\n",
-        "timestamp_micros: 98\nvalue: \"v\"\nvalue_size: 10\ncommit_row: false\n",
+        "timestamp_micros: 98\nvalue: \"v\"\nvalue_size: 11\ncommit_row: false\n",
         "value: \"alue-VAL_2\"\ncommit_row: true\n"
       ],
       "results": [
@@ -353,9 +353,9 @@
     {
       "name": "multi-qualifier splits",
       "chunks": [
-        "row_key: \"RK\"\nfamily_name: \u003c\n  value: \"A\"\n\u003e\nqualifier: \u003c\n  value: \"C\"\n\u003e\ntimestamp_micros: 99\nvalue: \"v\"\nvalue_size: 10\ncommit_row: false\n",
+        "row_key: \"RK\"\nfamily_name: \u003c\n  value: \"A\"\n\u003e\nqualifier: \u003c\n  value: \"C\"\n\u003e\ntimestamp_micros: 99\nvalue: \"v\"\nvalue_size: 11\ncommit_row: false\n",
         "value: \"alue-VAL_1\"\ncommit_row: false\n",
-        "qualifier: \u003c\n  value: \"D\"\n\u003e\ntimestamp_micros: 98\nvalue: \"v\"\nvalue_size: 10\ncommit_row: false\n",
+        "qualifier: \u003c\n  value: \"D\"\n\u003e\ntimestamp_micros: 98\nvalue: \"v\"\nvalue_size: 11\ncommit_row: false\n",
         "value: \"alue-VAL_2\"\ncommit_row: true\n"
       ],
       "results": [
@@ -382,11 +382,11 @@
     {
       "name": "multi-qualifier multi-split",
       "chunks": [
-        "row_key: \"RK\"\nfamily_name: \u003c\n  value: \"A\"\n\u003e\nqualifier: \u003c\n  value: \"C\"\n\u003e\ntimestamp_micros: 99\nvalue: \"v\"\nvalue_size: 10\ncommit_row: false\n",
-        "value: \"a\"\nvalue_size: 10\ncommit_row: false\n",
+        "row_key: \"RK\"\nfamily_name: \u003c\n  value: \"A\"\n\u003e\nqualifier: \u003c\n  value: \"C\"\n\u003e\ntimestamp_micros: 99\nvalue: \"v\"\nvalue_size: 11\ncommit_row: false\n",
+        "value: \"a\"\nvalue_size: 11\ncommit_row: false\n",
         "value: \"lue-VAL_1\"\ncommit_row: false\n",
-        "qualifier: \u003c\n  value: \"D\"\n\u003e\ntimestamp_micros: 98\nvalue: \"v\"\nvalue_size: 10\ncommit_row: false\n",
-        "value: \"a\"\nvalue_size: 10\ncommit_row: false\n",
+        "qualifier: \u003c\n  value: \"D\"\n\u003e\ntimestamp_micros: 98\nvalue: \"v\"\nvalue_size: 11\ncommit_row: false\n",
+        "value: \"a\"\nvalue_size: 11\ncommit_row: false\n",
         "value: \"lue-VAL_2\"\ncommit_row: true\n"
       ],
       "results": [
@@ -413,9 +413,9 @@
     {
       "name": "multi-family split",
       "chunks": [
-        "row_key: \"RK\"\nfamily_name: \u003c\n  value: \"A\"\n\u003e\nqualifier: \u003c\n  value: \"C\"\n\u003e\ntimestamp_micros: 99\nvalue: \"v\"\nvalue_size: 10\ncommit_row: false\n",
+        "row_key: \"RK\"\nfamily_name: \u003c\n  value: \"A\"\n\u003e\nqualifier: \u003c\n  value: \"C\"\n\u003e\ntimestamp_micros: 99\nvalue: \"v\"\nvalue_size: 11\ncommit_row: false\n",
         "value: \"alue-VAL_1\"\ncommit_row: false\n",
-        "family_name: \u003c\n  value: \"B\"\n\u003e\nqualifier: \u003c\n  value: \"E\"\n\u003e\ntimestamp_micros: 98\nvalue: \"v\"\nvalue_size: 10\ncommit_row: false\n",
+        "family_name: \u003c\n  value: \"B\"\n\u003e\nqualifier: \u003c\n  value: \"E\"\n\u003e\ntimestamp_micros: 98\nvalue: \"v\"\nvalue_size: 11\ncommit_row: false\n",
         "value: \"alue-VAL_2\"\ncommit_row: true\n"
       ],
       "results": [
@@ -818,9 +818,9 @@
     {
       "name": "two rows with splits, same timestamp",
       "chunks": [
-        "row_key: \"RK_1\"\nfamily_name: \u003c\n  value: \"A\"\n\u003e\nqualifier: \u003c\n  value: \"C\"\n\u003e\ntimestamp_micros: 100\nvalue: \"v\"\nvalue_size: 10\ncommit_row: false\n",
+        "row_key: \"RK_1\"\nfamily_name: \u003c\n  value: \"A\"\n\u003e\nqualifier: \u003c\n  value: \"C\"\n\u003e\ntimestamp_micros: 100\nvalue: \"v\"\nvalue_size: 11\ncommit_row: false\n",
         "value: \"alue-VAL_1\"\ncommit_row: true\n",
-        "row_key: \"RK_2\"\nfamily_name: \u003c\n  value: \"A\"\n\u003e\nqualifier: \u003c\n  value: \"C\"\n\u003e\ntimestamp_micros: 100\nvalue: \"v\"\nvalue_size: 10\ncommit_row: false\n",
+        "row_key: \"RK_2\"\nfamily_name: \u003c\n  value: \"A\"\n\u003e\nqualifier: \u003c\n  value: \"C\"\n\u003e\ntimestamp_micros: 100\nvalue: \"v\"\nvalue_size: 11\ncommit_row: false\n",
         "value: \"alue-VAL_2\"\ncommit_row: true\n"
       ],
       "results": [
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 3774cd2d6..edf5f1ee9 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
@@ -293,7 +293,7 @@ public class TestAsyncCheckAndMutate extends AbstractAsyncTest {
     delete = new Delete(rowKey).addColumns(SharedTestEnvRule.COLUMN_FAMILY, qual2);
     success = table.checkAndMutate(rowKey, SharedTestEnvRule.COLUMN_FAMILY)
         .qualifier(qual1)
-        .ifEquals(null)
+        .ifNotExists()
         .thenDelete(delete)
         .get();
     Assert.assertTrue(success);