Repository: GoogleCloudPlatform/cloud-bigtable-client
Pull request #1617 (2018-01-22 23:07:49)

Adding a wrapper for RowFilter
Filters is a new mechanism for creating `RowFilter` objects.  Most of the credit goes to @igorbernstein2.

@igorbernstein2, I had to include open/closed as first class citizens of Column and Value range filters.  I had to remove the notion of a Range.  Also, I replaced Truth with Assert, and used Long instead of log for timestmap range.

sduskis: The comments were addressed.  PTAL.
sduskis: I added private ctors.

Patch:
diff --git a/bigtable-client-core-parent/bigtable-client-core/src/main/java/com/google/cloud/bigtable/data/v2/internal/RegexUtil.java b/bigtable-client-core-parent/bigtable-client-core/src/main/java/com/google/cloud/bigtable/data/v2/internal/RegexUtil.java
new file mode 100644
index 000000000..0c9b147d6
--- /dev/null
+++ b/bigtable-client-core-parent/bigtable-client-core/src/main/java/com/google/cloud/bigtable/data/v2/internal/RegexUtil.java
@@ -0,0 +1,79 @@
+/*
+ * 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.internal;
+
+import com.google.api.core.InternalApi;
+import com.google.protobuf.ByteString;
+import com.google.protobuf.ByteString.ByteIterator;
+import com.google.protobuf.ByteString.Output;
+import java.io.IOException;
+import java.io.OutputStream;
+
+@InternalApi
+public final class RegexUtil {
+  private static final byte[] NULL_BYTES = "\\x00".getBytes();
+
+  private RegexUtil() {}
+
+  public static String literalRegex(final String value) {
+    return literalRegex(ByteString.copyFromUtf8(value)).toStringUtf8();
+  }
+  /** Converts the value to a quoted regular expression. */
+  public static ByteString literalRegex(ByteString value) {
+    Output output = ByteString.newOutput(value.size() * 2);
+
+    ByteIterator it = value.iterator();
+    try {
+      writeLiteralRegex(it, output);
+    } catch (IOException e) {
+      throw new RuntimeException("Unexpected io error converting regex", e);
+    }
+
+    return output.toByteString();
+  }
+
+  // Extracted from: re2 QuoteMeta:
+  // https://github.com/google/re2/blob/70f66454c255080a54a8da806c52d1f618707f8a/re2/re2.cc#L456
+  private static void writeLiteralRegex(ByteIterator input, OutputStream output)
+      throws IOException {
+    while (input.hasNext()) {
+      byte unquoted = input.nextByte();
+
+      if ((unquoted < 'a' || unquoted > 'z')
+          && (unquoted < 'A' || unquoted > 'Z')
+          && (unquoted < '0' || unquoted > '9')
+          && unquoted != '_'
+          &&
+          // If this is the part of a UTF8 or Latin1 character, we need
+          // to copy this byte without escaping.  Experimentally this is
+          // what works correctly with the regexp library.
+          (unquoted & 128) == 0) {
+
+        if (unquoted == '\0') { // Special handling for null chars.
+          // Note that this special handling is not strictly required for RE2,
+          // but this quoting is required for other regexp libraries such as
+          // PCRE.
+          // Can't use "\\0" since the next character might be a digit.
+          output.write(NULL_BYTES, 0, NULL_BYTES.length);
+          continue;
+        }
+
+        output.write('\\');
+      }
+      output.write(unquoted);
+    }
+  }
+}
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
new file mode 100644
index 000000000..9793b74e0
--- /dev/null
+++ b/bigtable-client-core-parent/bigtable-client-core/src/main/java/com/google/cloud/bigtable/data/v2/wrappers/Filters.java
@@ -0,0 +1,659 @@
+/*
+ * 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.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.protobuf.ByteString;
+
+/**
+ * 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 F
+ * and use its fluent API to build filters.
+ *
+ * <p>Sample code:
+ *
+ * <pre>{@code
+ * import static com.google.cloud.bigtable.data.v2.wrappers.Filters.F;
+ *
+ * void main() {
+ *   // Build the filter expression
+ *   RowFilter filter = F.chain()
+ *     .filter(F.qualifier().regex("prefix.*"))
+ *     .filter(F.limit().cellsPerRow(10));
+ * }
+ *
+ * }</pre>
+ */
+public final class Filters {
+  /** Entry point into the DSL. */
+  public static final Filters F = new Filters();
+
+  private static final SimpleFilter PASS =
+      new SimpleFilter(RowFilter.newBuilder().setPassAllFilter(true).build());
+  private static final SimpleFilter BLOCK =
+      new SimpleFilter(RowFilter.newBuilder().setBlockAllFilter(true).build());
+  private static final SimpleFilter SINK =
+      new SimpleFilter(RowFilter.newBuilder().setSink(true).build());
+  private static final SimpleFilter STRIP_VALUE =
+      new SimpleFilter(RowFilter.newBuilder().setStripValueTransformer(true).build());
+
+  private Filters() {}
+
+  /**
+   * Creates an empty chain filter list. Filters can be added to the chain by invoking {@link
+   * ChainFilter#filter(Filter)}.
+   *
+   * <p>The elements of "filters" are chained together to process the input row:
+   *
+   * <pre>{@code in row -> filter0 -> intermediate row -> filter1 -> ... -> filterN -> out row}
+   * </pre>
+   *
+   * The full chain is executed atomically.
+   */
+  public ChainFilter chain() {
+    return new ChainFilter();
+  }
+
+  /**
+   * Creates an empty interleave filter list. Filters can be added to the interleave by invoking
+   * {@link InterleaveFilter#filter(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
+   * column and timestamp, they will all appear in the output row in an unspecified mutual order.
+   * The full chain is executed atomically.
+   */
+  public InterleaveFilter interleave() {
+    return new InterleaveFilter();
+  }
+
+  /**
+   * 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)}.
+   *
+   * <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)}.
+   */
+  public ConditionFilter condition(Filter predicate) {
+    return new ConditionFilter(predicate);
+  }
+
+  /** Returns the builder for row key related filters. */
+  public KeyFilter key() {
+    return new KeyFilter();
+  }
+
+  /** Returns the builder for column family related filters. */
+  public FamilyFilter family() {
+    return new FamilyFilter();
+  }
+
+  /** Returns the builder for column qualifier related filters. */
+  public QualifierFilter qualifier() {
+    return new QualifierFilter();
+  }
+
+  /** Returns the builder for timestamp related filters. */
+  public TimestampFilter timestamp() {
+    return new TimestampFilter();
+  }
+
+  /** Returns the builder for value related filters. */
+  public ValueFilter value() {
+    return new ValueFilter();
+  }
+
+  /** Returns the builder for offset related filters. */
+  public OffsetFilter offset() {
+    return new OffsetFilter();
+  }
+
+  /** Returns the builder for limit related filters. */
+  public LimitFilter limit() {
+    return new LimitFilter();
+  }
+
+  // Miscellaneous filters without a clear target.
+  /** Matches all cells, regardless of input. Functionally equivalent to having no filter. */
+  public Filter pass() {
+    return PASS;
+  }
+
+  /**
+   * Does not match any cells, regardless of input. Useful for temporarily disabling just part of a
+   * filter.
+   */
+  public Filter block() {
+    return BLOCK;
+  }
+
+  /**
+   * Outputs all cells directly to the output of the read rather than to any parent filter. For
+   * advanced usage, see comments in
+   * https://github.com/googleapis/googleapis/blob/master/google/bigtable/v2/data.proto for more
+   * details.
+   */
+  public Filter sink() {
+    return SINK;
+  }
+
+  /**
+   * Applies the given label to all cells in the output row. This allows the caller to determine
+   * which results were produced from which part of the filter.
+   *
+   * <p>Due to a technical limitation, it is not currently possible to apply multiple labels to a
+   * cell. As a result, a {@link ChainFilter} may have no more than one sub-filter which contains a
+   * 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) {
+    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;
+
+    private ChainFilter() {
+      this(RowFilter.Chain.newBuilder());
+    }
+
+    private ChainFilter(RowFilter.Chain.Builder builder) {
+      this.builder = builder;
+    }
+
+    /** Add a filter to chain. */
+    public ChainFilter filter(Filter filter) {
+      builder.addFilters(filter.toProto());
+      return this;
+    }
+
+    @InternalApi
+    @Override
+    public RowFilter toProto() {
+      if (builder.getFiltersCount() == 1) {
+        return builder.getFilters(0);
+      } else {
+        return RowFilter.newBuilder().setChain(builder.build()).build();
+      }
+    }
+
+    /**
+     * Makes a deep copy of the Chain.
+     */
+    @Override
+    public ChainFilter clone() {
+      return new ChainFilter(builder.build().toBuilder());
+    }
+  }
+
+  /** DSL for adding filters to the interleave list. */
+  public static final class InterleaveFilter implements Filter {
+    RowFilter.Interleave.Builder builder = RowFilter.Interleave.newBuilder();
+
+    private InterleaveFilter() {
+    }
+
+    /**
+     * Adds a {@link Filter} to the interleave list.
+     */
+    public InterleaveFilter filter(Filter filter) {
+      builder.addFilters(filter.toProto());
+      return this;
+    }
+
+    @InternalApi
+    @Override
+    public RowFilter toProto() {
+      if (builder.getFiltersCount() == 1) {
+        return builder.getFilters(0);
+      } else {
+        return RowFilter.newBuilder().setInterleave(builder.build()).build();
+      }
+    }
+  }
+
+  /** DSL for configuring a conditional filter. */
+  public static final class ConditionFilter implements Filter {
+    private RowFilter.Condition.Builder builder = RowFilter.Condition.newBuilder();
+
+    private ConditionFilter() {
+    }
+
+    private ConditionFilter(Filter predicate) {
+      builder.setPredicateFilter(predicate.toProto());
+    }
+
+    /** Sets (replaces) the filter to apply when the predicate is true. */
+    public ConditionFilter then(Filter filter) {
+      builder.setTrueFilter(filter.toProto());
+      return this;
+    }
+
+    /** Sets (replaces) the filter to apply when the predicate is false. */
+    public ConditionFilter otherwise(Filter filter) {
+      builder.setFalseFilter(filter.toProto());
+      return this;
+    }
+
+    @InternalApi
+    @Override
+    public RowFilter toProto() {
+      return RowFilter.newBuilder().setCondition(builder.build()).build();
+    }
+  }
+
+  public static final class 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
+     * 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) {
+      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
+     * 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());
+    }
+
+    /**
+     * 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());
+    }
+
+    /**
+     * Matches all cells from a row with `probability`, and matches no cells from the row with
+     * probability 1-`probability`.
+     */
+    public Filter sample(double probability) {
+      return new SimpleFilter(RowFilter.newBuilder().setRowSampleFilter(probability).build());
+    }
+  }
+
+  public static final class 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
+     * 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) {
+      return new SimpleFilter(RowFilter.newBuilder().setFamilyNameRegexFilter(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 static final class QualifierFilter {
+
+    private QualifierFilter() {
+    }
+
+    /**
+     * Matches only cells from columns whose qualifiers satisfy the given <a
+     * href="https://github.com/google/re2/wiki/Syntax>RE2 regex</a>. Note that, since column
+     * qualifiers 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 qualifier.
+     */
+    public Filter regex(String regex) {
+      return regex(ByteString.copyFromUtf8(regex));
+    }
+
+    /**
+     * Matches only cells from columns whose qualifiers satisfy the given <a
+     * href="https://github.com/google/re2/wiki/Syntax>RE2 regex</a>. Note that, since column
+     * qualifiers 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 qualifier.
+     */
+    public Filter regex(ByteString regex) {
+      return new SimpleFilter(
+          RowFilter.newBuilder()
+              .setColumnQualifierRegexFilter(regex)
+              .build());
+    }
+
+    /** Matches only cells from columns whose qualifiers equal the value. */
+    public Filter exactMatch(ByteString 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 range(String 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();
+
+    private TimestampRangeFilter() {
+    }
+
+    /**
+     * 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;
+    }
+
+    /**
+     * 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;
+    }
+
+    @InternalApi
+    @Override
+    public RowFilter toProto() {
+      return RowFilter.newBuilder().setTimestampRangeFilter(range.build()).build();
+    }
+  }
+
+  public static final class TimestampFilter {
+
+    private TimestampFilter() {
+    }
+
+    /**
+     * Matches only cells with timestamps within the given range.
+     *
+     * @return a {@link TimestampRangeFilter} on which start / end timestamps can be specified.
+     */
+    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;
+    }
+
+    @InternalApi
+    @Override
+    public RowFilter toProto() {
+      return RowFilter.newBuilder().setValueRangeFilter(range.build()).build();
+    }
+  }
+
+  public static final class ValueFilter {
+
+    private ValueFilter() {
+    }
+
+    /**
+     * Matches only cells with values that satisfy the given <a
+     * href="https://github.com/google/re2/wiki/Syntax>RE2 regex</a>. Note that, since cell values
+     * 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) {
+      return regex(ByteString.copyFromUtf8(regex));
+    }
+
+    /** Matches only cells with values that match the given value. */
+    public Filter exactMatch(ByteString value) {
+      return regex(RegexUtil.literalRegex(value));
+    }
+
+    /**
+     * Matches only cells with values that satisfy the given <a
+     * href="https://github.com/google/re2/wiki/Syntax>RE2 regex</a>. Note that, since cell values
+     * 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) {
+      return new SimpleFilter(RowFilter.newBuilder().setValueRegexFilter(regex).build());
+    }
+
+    /**
+     * Construct a {@link ValueRangeBuilder} that can create a {@link ValueRange} oriented
+     * {@link Filter}.
+     * @return a new {@link ValueRangeBuilder}
+     */
+    public ValueRangeFilter range() {
+      return new ValueRangeFilter();
+    }
+
+    /** Replaces each cell's value with the empty string. */
+    public Filter strip() {
+      return STRIP_VALUE;
+    }
+  }
+
+  public static final class OffsetFilter {
+
+    private OffsetFilter() {
+    }
+
+    /**
+     * Skips the first N cells of each row, matching all subsequent cells. If duplicate cells are
+     * present, as is possible when using an {@link InterleaveFilter}, each copy of the cell is
+     * counted separately.
+     */
+    public Filter cellsPerRow(int count) {
+      return new SimpleFilter(RowFilter.newBuilder().setCellsPerRowOffsetFilter(count).build());
+    }
+  }
+
+  public static final class LimitFilter {
+
+    private LimitFilter() {
+    }
+
+    /**
+     * Matches only the first N cells of each row. If duplicate cells are present, as is possible
+     * when using an Interleave, each copy of the cell is counted separately.
+     */
+    public Filter cellsPerRow(int count) {
+      return new SimpleFilter(RowFilter.newBuilder().setCellsPerRowLimitFilter(count).build());
+    }
+
+    /**
+     * Matches only the most recent `count` cells within each column. For example, if count=2, this
+     * filter would match column `foo:bar` at timestamps 10 and 9 skip all earlier cells in
+     * `foo:bar`, and then begin matching again in column `foo:bar2`. If duplicate cells are
+     * present, as is possible when using an {@link InterleaveFilter}, each copy of the cell is
+     * counted separately.
+     */
+    public Filter cellsPerColumn(int count) {
+      return new SimpleFilter(RowFilter.newBuilder().setCellsPerColumnLimitFilter(count).build());
+    }
+  }
+
+  private static final class SimpleFilter implements Filter {
+
+    private final RowFilter proto;
+
+    private SimpleFilter(RowFilter proto) {
+      this.proto = proto;
+    }
+
+    @InternalApi
+    @Override
+    public RowFilter toProto() {
+      return proto;
+    }
+  }
+
+  public interface Filter {
+    @InternalApi
+    public abstract RowFilter toProto();
+  }
+}


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
new file mode 100644
index 000000000..5f34a9f2d
--- /dev/null
+++ b/bigtable-client-core-parent/bigtable-client-core/src/test/java/com/google/cloud/bigtable/data/v2/wrappers/FiltersTest.java
@@ -0,0 +1,411 @@
+/*
+ * 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 static com.google.cloud.bigtable.data.v2.wrappers.Filters.F;
+
+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.ValueRange;
+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 FiltersTest {
+  @Test
+  public void chainTest() {
+    RowFilter actualProto =
+        F.chain()
+            .filter(F.key().regex(".*"))
+            .filter(F.key().sample(0.5))
+            .filter(F.chain()
+                .filter(F.family().regex("hi$"))
+                .filter(F.qualifier().regex("^q")))
+            .toProto();
+
+    RowFilter expectedFilter =
+        RowFilter.newBuilder()
+            .setChain(
+                Chain.newBuilder()
+                    .addFilters(
+                        RowFilter.newBuilder().setRowKeyRegexFilter(ByteString.copyFromUtf8(".*")))
+                    .addFilters(RowFilter.newBuilder().setRowSampleFilter(0.5))
+                    .addFilters(
+                        RowFilter.newBuilder()
+                            .setChain(
+                                Chain.newBuilder()
+                                    .addFilters(
+                                        RowFilter.newBuilder().setFamilyNameRegexFilter("hi$"))
+                                    .addFilters(
+                                        RowFilter.newBuilder().setColumnQualifierRegexFilter(
+                                            ByteString.copyFromUtf8("^q"))))))
+            .build();
+
+    Assert.assertEquals(expectedFilter, actualProto);
+  }
+
+  @Test
+  public void interleaveTest() {
+    RowFilter actualProto =
+        F.interleave()
+            .filter(F.key().regex(".*"))
+            .filter(F.key().sample(0.5))
+            .filter(F.interleave()
+                .filter(F.family().regex("hi$"))
+                .filter(F.qualifier().regex("^q")))
+            .toProto();
+
+    RowFilter expectedFilter =
+        RowFilter.newBuilder()
+            .setInterleave(
+                Interleave.newBuilder()
+                    .addFilters(
+                        RowFilter.newBuilder().setRowKeyRegexFilter(ByteString.copyFromUtf8(".*")))
+                    .addFilters(RowFilter.newBuilder().setRowSampleFilter(0.5))
+                    .addFilters(
+                        RowFilter.newBuilder()
+                            .setInterleave(
+                                Interleave.newBuilder()
+                                    .addFilters(
+                                        RowFilter.newBuilder().setFamilyNameRegexFilter("hi$"))
+                                    .addFilters(
+                                        RowFilter.newBuilder().setColumnQualifierRegexFilter(
+                                            ByteString.copyFromUtf8("^q"))))))
+            .build();
+
+    Assert.assertEquals(expectedFilter, actualProto);
+  }
+
+  @Test
+  public void conditionTest() {
+    RowFilter actualProto =
+        F.condition(F.key().regex(".*"))
+            .then(F.label("true"))
+            .otherwise(F.label("false"))
+            .toProto();
+
+    RowFilter expectedFilter =
+        RowFilter.newBuilder()
+            .setCondition(
+                Condition.newBuilder()
+                    .setPredicateFilter(
+                        RowFilter.newBuilder().setRowKeyRegexFilter(ByteString.copyFromUtf8(".*")))
+                    .setTrueFilter(RowFilter.newBuilder().setApplyLabelTransformer("true"))
+                    .setFalseFilter(RowFilter.newBuilder().setApplyLabelTransformer("false")))
+            .build();
+
+    Assert.assertEquals(expectedFilter, actualProto);
+  }
+
+  @Test
+  public void passTest() {
+    RowFilter actualProto = F.pass().toProto();
+
+    RowFilter expectedFilter = RowFilter.newBuilder().setPassAllFilter(true).build();
+
+    Assert.assertEquals(expectedFilter, actualProto);
+  }
+
+  @Test
+  public void blockTest() {
+    RowFilter actualProto = F.block().toProto();
+
+    RowFilter expectedFilter = RowFilter.newBuilder().setBlockAllFilter(true).build();
+
+    Assert.assertEquals(expectedFilter, actualProto);
+  }
+
+  @Test
+  public void sinkTest() {
+    RowFilter actualProto = F.sink().toProto();
+
+    RowFilter expectedFilter = RowFilter.newBuilder().setSink(true).build();
+
+    Assert.assertEquals(expectedFilter, actualProto);
+  }
+
+  @Test
+  public void labelTest() {
+    RowFilter actualProto = F.label("my-label").toProto();
+
+    RowFilter expectedFilter = RowFilter.newBuilder().setApplyLabelTransformer("my-label").build();
+
+    Assert.assertEquals(expectedFilter, actualProto);
+  }
+
+  @Test
+  public void keyRegexTest() {
+    RowFilter actualProto = F.key().regex(".*").toProto();
+
+    RowFilter expectedFilter =
+        RowFilter.newBuilder().setRowKeyRegexFilter(ByteString.copyFromUtf8(".*")).build();
+
+    Assert.assertEquals(expectedFilter, actualProto);
+  }
+
+  @Test
+  public void keyExactMatchTest() {
+    RowFilter actualProto = F.key().exactMatch(ByteString.copyFromUtf8(".*")).toProto();
+
+    RowFilter expectedFilter =
+        RowFilter.newBuilder().setRowKeyRegexFilter(ByteString.copyFromUtf8("\\.\\*")).build();
+
+    Assert.assertEquals(expectedFilter, actualProto);
+  }
+
+  @Test
+  public void keySampleTest() {
+    RowFilter actualProto = F.key().sample(0.3).toProto();
+
+    RowFilter expectedFilter = RowFilter.newBuilder().setRowSampleFilter(0.3).build();
+
+    Assert.assertEquals(expectedFilter, actualProto);
+  }
+
+  @Test
+  public void familyRegexTest() {
+    RowFilter actualProto = F.family().regex("^hi").toProto();
+
+    RowFilter expectedFilter = RowFilter.newBuilder().setFamilyNameRegexFilter("^hi").build();
+
+    Assert.assertEquals(expectedFilter, actualProto);
+  }
+
+  @Test
+  public void familyExactMatchTest() {
+    RowFilter actualProto = F.family().exactMatch("^hi").toProto();
+
+    RowFilter expectedFilter = RowFilter.newBuilder().setFamilyNameRegexFilter("\\^hi").build();
+
+    Assert.assertEquals(expectedFilter, actualProto);
+  }
+
+  @Test
+  public void qualifierRegexTest() {
+    RowFilter actualProto = F.qualifier().regex("^hi").toProto();
+
+    RowFilter expectedFilter =
+        RowFilter.newBuilder()
+            .setColumnQualifierRegexFilter(ByteString.copyFromUtf8("^hi"))
+            .build();
+
+    Assert.assertEquals(expectedFilter, actualProto);
+  }
+
+  @Test
+  public void qualifierExactMatchTest() {
+    RowFilter actualProto = F.qualifier().exactMatch(ByteString.copyFromUtf8("^hi")).toProto();
+
+    RowFilter expectedFilter =
+        RowFilter.newBuilder()
+            .setColumnQualifierRegexFilter(ByteString.copyFromUtf8("\\^hi"))
+            .build();
+
+    Assert.assertEquals(expectedFilter, actualProto);
+  }
+
+  @Test
+  public void qualifierRangeOpenClosed() {
+    String family = "family";
+    ByteString begin = ByteString.copyFromUtf8("begin");
+    ByteString end = ByteString.copyFromUtf8("end");
+    RowFilter actualProto = F.qualifier().range(family)
+        .startOpen(begin)
+        .endClosed(end)
+        .toProto();
+    RowFilter expectedFilter =
+        RowFilter.newBuilder()
+            .setColumnRangeFilter(
+                ColumnRange.newBuilder()
+                    .setFamilyName(family)
+                    .setStartQualifierOpen(begin)
+                    .setEndQualifierClosed(end))
+            .build();
+
+    Assert.assertEquals(expectedFilter, actualProto);
+  }
+
+  @Test
+  public void qualifierRangeClosedOpen() {
+    String family = "family";
+    ByteString begin = ByteString.copyFromUtf8("begin");
+    ByteString end = ByteString.copyFromUtf8("end");
+    RowFilter actualProto = F.qualifier().range(family)
+        .startClosed(begin)
+        .endOpen(end)
+        .toProto();
+    RowFilter expectedFilter =
+        RowFilter.newBuilder()
+            .setColumnRangeFilter(
+                ColumnRange.newBuilder()
+                    .setFamilyName(family)
+                    .setStartQualifierClosed(begin)
+                    .setEndQualifierOpen(end))
+            .build();
+
+    Assert.assertEquals(expectedFilter, actualProto);
+  }
+
+  @Test
+  public void valueRegex() {
+    RowFilter actualProto = F.value().regex("some[0-9]regex").toProto();
+
+    RowFilter expectedFilter =
+        RowFilter.newBuilder()
+            .setValueRegexFilter(ByteString.copyFromUtf8("some[0-9]regex"))
+            .build();
+
+    Assert.assertEquals(expectedFilter, actualProto);
+  }
+
+  @Test
+  public void valueExactMatch() {
+    RowFilter actualProto =
+        F.value().exactMatch(ByteString.copyFromUtf8("some[0-9]regex")).toProto();
+
+    RowFilter expectedFilter =
+        RowFilter.newBuilder()
+            .setValueRegexFilter(ByteString.copyFromUtf8("some\\[0\\-9\\]regex"))
+            .build();
+
+    Assert.assertEquals(expectedFilter, actualProto);
+  }
+
+  @Test
+  public void valueRangeClosedOpen() {
+    ByteString begin = ByteString.copyFromUtf8("begin");
+    ByteString end = ByteString.copyFromUtf8("end");
+
+    RowFilter actualProto = F.value().range()
+        .startOpen(begin)
+        .endClosed(end)
+        .toProto();
+
+    RowFilter expectedFilter =
+        RowFilter.newBuilder()
+            .setValueRangeFilter(
+                ValueRange.newBuilder()
+                    .setStartValueOpen(begin)
+                    .setEndValueClosed(end))
+            .build();
+
+    Assert.assertEquals(expectedFilter, actualProto);
+  }
+
+  @Test
+  public void valueRangeOpenClosed() {
+    ByteString begin = ByteString.copyFromUtf8("begin");
+    ByteString end = ByteString.copyFromUtf8("end");
+
+    RowFilter actualProto = F.value().range()
+        .startClosed(begin)
+        .endOpen(end)
+        .toProto();
+
+    RowFilter expectedFilter =
+        RowFilter.newBuilder()
+            .setValueRangeFilter(
+                ValueRange.newBuilder()
+                    .setStartValueClosed(begin)
+                    .setEndValueOpen(end))
+            .build();
+
+    Assert.assertEquals(expectedFilter, actualProto);
+  }
+
+  @Test
+  public void valueStripTest() {
+    RowFilter actualProto = F.value().strip().toProto();
+
+    RowFilter expectedFilter = RowFilter.newBuilder().setStripValueTransformer(true).build();
+
+    Assert.assertEquals(expectedFilter, actualProto);
+  }
+
+  @Test
+  public void offsetCellsPerRowTest() {
+    RowFilter actualProto = F.offset().cellsPerRow(10).toProto();
+
+    RowFilter expectedFilter = RowFilter.newBuilder().setCellsPerRowOffsetFilter(10).build();
+
+    Assert.assertEquals(expectedFilter, actualProto);
+  }
+
+  @Test
+  public void limitCellsPerRowTest() {
+    RowFilter actualProto = F.limit().cellsPerRow(10).toProto();
+
+    RowFilter expectedFilter = RowFilter.newBuilder().setCellsPerRowLimitFilter(10).build();
+
+    Assert.assertEquals(expectedFilter, actualProto);
+  }
+
+  @Test
+  public void limitCellsPerColumnTest() {
+    RowFilter actualProto = F.limit().cellsPerColumn(10).toProto();
+
+    RowFilter expectedFilter = RowFilter.newBuilder().setCellsPerColumnLimitFilter(10).build();
+
+    Assert.assertEquals(expectedFilter, actualProto);
+  }
+
+  @Test
+  public void timestampFullRangeTest() {
+    long start = TimeUnit.MILLISECONDS.toMicros(System.currentTimeMillis() - 10000000);
+    long end = TimeUnit.MILLISECONDS.toMicros(System.currentTimeMillis());
+    RowFilter actualProto = F.timestamp().range(start, end).toProto();
+
+    RowFilter.Builder expected = RowFilter.newBuilder();
+    expected.getTimestampRangeFilterBuilder()
+      .setStartTimestampMicros(start)
+      .setEndTimestampMicros(end);
+
+    Assert.assertEquals(expected.build(), actualProto);
+  }
+
+  @Test
+  public void timestampStartTest() {
+    long start = TimeUnit.MILLISECONDS.toMicros(System.currentTimeMillis() - 10000000);
+    RowFilter actualProto = F.timestamp().range().startClosed(start).toProto();
+
+    RowFilter.Builder expected = RowFilter.newBuilder();
+    expected.getTimestampRangeFilterBuilder()
+      .setStartTimestampMicros(start);
+
+    Assert.assertEquals(expected.build(), actualProto);
+  }
+
+  @Test
+  public void timestampEndTest() {
+    long end = TimeUnit.MILLISECONDS.toMicros(System.currentTimeMillis());
+    RowFilter actualProto = F.timestamp().range().endOpen(end).toProto();
+
+    RowFilter.Builder expected = RowFilter.newBuilder();
+    expected.getTimestampRangeFilterBuilder()
+      .setEndTimestampMicros(end);
+
+    Assert.assertEquals(expected.build(), actualProto);
+  }
+}