readRowsSettings() {
+ return getTypedStubSettings().readRowsSettings();
+ }
+
@SuppressWarnings("unchecked")
private EnhancedBigtableStubSettings.Builder getTypedStubSettings() {
return (EnhancedBigtableStubSettings.Builder) getStubSettings();
diff --git a/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/internal/RegexUtil.java b/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/internal/RegexUtil.java
new file mode 100644
index 000000000000..c348ec740814
--- /dev/null
+++ b/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/internal/RegexUtil.java
@@ -0,0 +1,83 @@
+/*
+ * 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;
+
+/**
+ * Contains utilities to handle RE2 flavor regular expressions. This differs from {@link
+ * java.util.regex.Pattern} in two important ways:
+ *
+ *
+ * - Binary strings are supported.
+ *
- The syntax is a lot more restricted but allows different modifiers.
+ *
+ *
+ * See https://github.com/google/re2 for more
+ * details.
+ */
+@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) {
+ ByteString.Output output = ByteString.newOutput(value.size() * 2);
+
+ ByteIterator it = value.iterator();
+ writeLiteralRegex(it, output);
+
+ return output.toByteString();
+ }
+
+ // Extracted from: re2 QuoteMeta:
+ // https://github.com/google/re2/blob/70f66454c255080a54a8da806c52d1f618707f8a/re2/re2.cc#L456
+ private static void writeLiteralRegex(ByteIterator input, ByteString.Output output) {
+ 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/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/stub/EnhancedBigtableStub.java b/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/stub/EnhancedBigtableStub.java
index 0acd0b53ca2b..cae90fe50565 100644
--- a/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/stub/EnhancedBigtableStub.java
+++ b/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/stub/EnhancedBigtableStub.java
@@ -16,8 +16,15 @@
package com.google.cloud.bigtable.data.v2.stub;
import com.google.api.core.InternalApi;
+import com.google.api.gax.rpc.ApiCallContext;
import com.google.api.gax.rpc.ClientContext;
+import com.google.api.gax.rpc.ResponseObserver;
+import com.google.api.gax.rpc.ServerStreamingCallable;
import com.google.cloud.bigtable.data.v2.internal.RequestContext;
+import com.google.cloud.bigtable.data.v2.wrappers.DefaultRowAdapter;
+import com.google.cloud.bigtable.data.v2.wrappers.Query;
+import com.google.cloud.bigtable.data.v2.wrappers.Row;
+import com.google.cloud.bigtable.data.v2.wrappers.RowAdapter;
import java.io.IOException;
/**
@@ -39,6 +46,8 @@ public class EnhancedBigtableStub implements AutoCloseable {
private final ClientContext clientContext;
private final RequestContext requestContext;
+ private final ServerStreamingCallable readRowsCallable;
+
public static EnhancedBigtableStub create(EnhancedBigtableStubSettings settings)
throws IOException {
// Configure the base settings
@@ -63,16 +72,31 @@ public static EnhancedBigtableStub create(EnhancedBigtableStubSettings settings)
this.stub = stub;
this.requestContext =
RequestContext.create(settings.getInstanceName(), settings.getAppProfileId());
- }
- @Override
- public void close() throws Exception {
- stub.close();
+ readRowsCallable = createReadRowsCallable(new DefaultRowAdapter());
}
//
+ public ServerStreamingCallable createReadRowsCallable(
+ RowAdapter rowAdapter) {
+ return new ServerStreamingCallable() {
+ @Override
+ public void call(
+ Query query, ResponseObserver responseObserver, ApiCallContext context) {
+ throw new UnsupportedOperationException("todo");
+ }
+ };
+ }
//
//
+ public ServerStreamingCallable readRowsCallable() {
+ return readRowsCallable;
+ }
//
+
+ @Override
+ public void close() throws Exception {
+ stub.close();
+ }
}
diff --git a/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/stub/EnhancedBigtableStubSettings.java b/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/stub/EnhancedBigtableStubSettings.java
index 8efa594a0d5c..72f8bfeddbf1 100644
--- a/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/stub/EnhancedBigtableStubSettings.java
+++ b/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/stub/EnhancedBigtableStubSettings.java
@@ -17,11 +17,17 @@
import com.google.api.core.InternalApi;
import com.google.api.gax.grpc.InstantiatingGrpcChannelProvider;
+import com.google.api.gax.retrying.RetrySettings;
+import com.google.api.gax.rpc.ServerStreamingCallSettings;
+import com.google.api.gax.rpc.StatusCode.Code;
import com.google.api.gax.rpc.StubSettings;
import com.google.api.gax.rpc.UnaryCallSettings;
import com.google.bigtable.admin.v2.InstanceName;
+import com.google.cloud.bigtable.data.v2.wrappers.Query;
+import com.google.cloud.bigtable.data.v2.wrappers.Row;
import com.google.common.base.Preconditions;
import javax.annotation.Nonnull;
+import org.threeten.bp.Duration;
/**
* Settings class to configure an instance of {@link EnhancedBigtableStub}.
@@ -40,10 +46,14 @@
* build() is called, the tree of builders is called to create the complete settings object.
*
* {@code
- * BigtableDataSettings bigtableDataSettings = BigtableDataSettings.newBuilder()
+ * BigtableDataSettings.Builder settingsBuilder = BigtableDataSettings.newBuilder()
* .setInstanceName(InstanceName.of("my-project", "my-instance-id"))
- * .setAppProfileId("default")
- * .build();
+ * .setAppProfileId("default");
+ *
+ * settingsBuilder.readRowsSettings()
+ * .setRetryableCodes(Code.DEADLINE_EXCEEDED, Code.UNAVAILABLE);
+ *
+ * BigtableDataSettings settings = builder.build();
* }
*
* This class is considered an internal implementation detail and not meant to be used by
@@ -58,14 +68,18 @@ public class EnhancedBigtableStubSettings extends StubSettings readRowsSettings;
+
private EnhancedBigtableStubSettings(Builder builder) {
super(builder);
instanceName = builder.instanceName;
appProfileId = builder.appProfileId;
// Per method settings.
+ readRowsSettings = builder.readRowsSettings.build();
}
+ /** Create a new builder. */
public static Builder newBuilder() {
return new Builder();
}
@@ -80,6 +94,11 @@ public String getAppProfileId() {
return appProfileId;
}
+ /** Returns the object with the settings used for calls to ReadRows. */
+ public ServerStreamingCallSettings readRowsSettings() {
+ return readRowsSettings;
+ }
+
/** Returns a builder containing all the values of this settings class. */
public Builder toBuilder() {
return new Builder(this);
@@ -90,6 +109,8 @@ public static class Builder extends StubSettings.Builder readRowsSettings;
+
/**
* Initializes a new Builder with sane defaults for all settings.
*
@@ -114,6 +135,23 @@ private Builder() {
.build());
// Per-method settings using baseSettings for defaults.
+ readRowsSettings = ServerStreamingCallSettings.newBuilder();
+ /* TODO: copy timeouts, retryCodes & retrySettings from baseSettings.readRows once it exists in GAPIC */
+ readRowsSettings
+ .setRetryableCodes(Code.DEADLINE_EXCEEDED, Code.UNAVAILABLE, Code.ABORTED)
+ .setTimeoutCheckInterval(Duration.ofSeconds(10))
+ .setIdleTimeout(Duration.ofMinutes(5))
+ .setRetrySettings(
+ RetrySettings.newBuilder()
+ .setMaxAttempts(10)
+ .setTotalTimeout(Duration.ofHours(1))
+ .setInitialRetryDelay(Duration.ofMillis(100))
+ .setRetryDelayMultiplier(1.3)
+ .setMaxRetryDelay(Duration.ofMinutes(1))
+ .setInitialRpcTimeout(Duration.ofSeconds(20))
+ .setRpcTimeoutMultiplier(1)
+ .setMaxRpcTimeout(Duration.ofSeconds(20))
+ .build());
}
private Builder(EnhancedBigtableStubSettings settings) {
@@ -122,6 +160,7 @@ private Builder(EnhancedBigtableStubSettings settings) {
appProfileId = settings.appProfileId;
// Per method settings.
+ readRowsSettings = settings.readRowsSettings.toBuilder();
}
//
@@ -169,6 +208,11 @@ public String getAppProfileId() {
return appProfileId;
}
+ /** Returns the builder for the settings used for calls to readRows. */
+ public ServerStreamingCallSettings.Builder readRowsSettings() {
+ return readRowsSettings;
+ }
+
@SuppressWarnings("unchecked")
public EnhancedBigtableStubSettings build() {
Preconditions.checkState(instanceName != null, "InstanceName must be set");
diff --git a/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/wrappers/DefaultRowAdapter.java b/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/wrappers/DefaultRowAdapter.java
new file mode 100644
index 000000000000..2bfcf688a4f8
--- /dev/null
+++ b/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/wrappers/DefaultRowAdapter.java
@@ -0,0 +1,108 @@
+/*
+ * 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.collect.ImmutableList;
+import com.google.protobuf.ByteString;
+import java.util.List;
+
+/**
+ * Default implementation of a {@link RowAdapter} that uses {@link Row}s to represent logical rows.
+ */
+public class DefaultRowAdapter implements RowAdapter {
+ /** {@inheritDoc} */
+ @Override
+ public boolean isScanMarkerRow(Row row) {
+ return row.getCells().isEmpty();
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public RowBuilder createRowBuilder() {
+ return new DefaultRowBuilder();
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public ByteString getKey(Row row) {
+ return row.getKey();
+ }
+
+ /** {@inheritDoc} */
+ public class DefaultRowBuilder implements RowBuilder {
+ private ByteString currentKey;
+ private ImmutableList.Builder cells;
+ private String family;
+ private ByteString qualifier;
+ private List labels;
+ private long timestamp;
+ private ByteString value;
+
+ /** {@inheritDoc} */
+ @Override
+ public Row createScanMarkerRow(ByteString key) {
+ return Row.create(key, ImmutableList.of());
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public void startRow(ByteString key) {
+ currentKey = key;
+ cells = ImmutableList.builder();
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public void startCell(
+ String family, ByteString qualifier, long timestamp, List labels, long size) {
+ this.family = family;
+ this.qualifier = qualifier;
+ this.timestamp = timestamp;
+ this.labels = labels;
+ this.value = ByteString.EMPTY;
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public void cellValue(ByteString value) {
+ this.value = this.value.concat(value);
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public void finishCell() {
+ cells.add(RowCell.create(family, qualifier, timestamp, labels, value));
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public Row finishRow() {
+ return Row.create(currentKey, cells.build());
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public void reset() {
+ currentKey = null;
+ cells = null;
+ family = null;
+ qualifier = null;
+ labels = null;
+ timestamp = 0;
+ value = null;
+ }
+ }
+}
diff --git a/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/wrappers/Filters.java b/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/wrappers/Filters.java
new file mode 100644
index 000000000000..b279d6ae56cc
--- /dev/null
+++ b/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/wrappers/Filters.java
@@ -0,0 +1,746 @@
+/*
+ * 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.core.InternalApi;
+import com.google.bigtable.v2.ColumnRange;
+import com.google.bigtable.v2.RowFilter;
+import com.google.bigtable.v2.RowFilter.Condition;
+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;
+import javax.annotation.Nullable;
+
+/**
+ * A Fluent DSL to create a hierarchy of filters for the CheckAndMutateRow RPCs and ReadRows Query.
+ *
+ * Intended usage is to statically import, or in case of conflict, assign the static variable
+ * FILTERS and use its fluent API to build filters.
+ *
+ *
Sample code:
+ *
+ *
{@code
+ * import static com.google.cloud.bigtable.data.v2.wrappers.Filters.FILTERS;
+ *
+ * void main() {
+ * // Build the filter expression
+ * RowFilter filter = FILTERS.chain()
+ * .filter(FILTERS.qualifier().regex("prefix.*"))
+ * .filter(FILTERS.limit().cellsPerRow(10));
+ *
+ * // Use it in a Query
+ * Query query = Query.create("[TABLE]")
+ * .filter(filter);
+ * }
+ *
+ * }
+ */
+public final class Filters {
+ /** Entry point into the DSL. */
+ @SuppressWarnings("WeakerAccess")
+ public static final Filters FILTERS = 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(Filters.Filter)}.
+ *
+ * The elements of "filters" are chained together to process the input row:
+ *
+ *
{@code in row -> filter0 -> intermediate row -> filter1 -> ... -> filterN -> out row}
+ *
+ *
+ * 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(Filters.Filter)}.
+ *
+ * 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(Filters.Filter)} and {@link
+ * ConditionFilter#otherwise(Filters.Filter)}.
+ *
+ *
A RowFilter which evaluates one of two possible RowFilters, depending on whether or not a
+ * predicate RowFilter outputs any cells from the input row.
+ *
+ *
IMPORTANT NOTE: The predicate filter does not execute atomically with the {@link
+ * 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(@Nonnull Filter predicate) {
+ Preconditions.checkNotNull(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.
+ *
+ *
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(@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 {
+ private RowFilter.Chain.Builder builder;
+
+ private ChainFilter() {
+ this.builder = RowFilter.Chain.newBuilder();
+ }
+
+ /** Add a filter to chain. */
+ public ChainFilter filter(@Nonnull Filter filter) {
+ Preconditions.checkNotNull(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() {
+ 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 {
+ private RowFilter.Interleave.Builder builder;
+
+ private InterleaveFilter() {
+ builder = RowFilter.Interleave.newBuilder();
+ }
+
+ /** Adds a {@link Filter} to the interleave list. */
+ public InterleaveFilter filter(@Nonnull Filter filter) {
+ Preconditions.checkNotNull(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();
+ }
+ }
+
+ @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;
+
+ private ConditionFilter(@Nonnull Filter predicate) {
+ Preconditions.checkNotNull(predicate);
+ builder = Condition.newBuilder().setPredicateFilter(predicate.toProto());
+ }
+
+ /** Sets (replaces) the filter to apply when the predicate is true. */
+ 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(@Nonnull Filter filter) {
+ Preconditions.checkNotNull(filter);
+ builder.setFalseFilter(filter.toProto());
+ return this;
+ }
+
+ @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() {}
+
+ /**
+ * Matches only cells from rows whose keys satisfy the given RE2 regex. 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(@Nonnull String regex) {
+ Preconditions.checkNotNull(regex);
+ return regex(wrapString(regex));
+ }
+
+ /**
+ * Matches only cells from rows whose keys satisfy the given RE2 regex. 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(@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(@Nonnull ByteString value) {
+ Preconditions.checkNotNull(value);
+
+ return regex(RegexUtil.literalRegex(value));
+ }
+
+ /**
+ * Matches all cells from a row with `probability`, and matches no cells from the row with
+ * 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() {}
+
+ /**
+ * Matches only cells from columns whose families satisfy the given RE2 regex. 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(@Nonnull String regex) {
+ Preconditions.checkNotNull(regex);
+ return new SimpleFilter(RowFilter.newBuilder().setFamilyNameRegexFilter(regex).build());
+ }
+
+ /** Matches only cells from columns whose families match the value. */
+ public Filter exactMatch(@Nonnull String value) {
+ Preconditions.checkNotNull(value);
+ return regex(RegexUtil.literalRegex(value));
+ }
+ }
+
+ public static final class QualifierFilter {
+ private QualifierFilter() {}
+
+ /**
+ * Matches only cells from columns whose qualifiers satisfy the given RE2 regex. 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(@Nonnull String regex) {
+ return regex(wrapString(regex));
+ }
+
+ /**
+ * Matches only cells from columns whose qualifiers satisfy the given RE2 regex. 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(@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(@Nonnull 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 rangeWithinFamily(@Nonnull String family) {
+ Preconditions.checkNotNull(family);
+ return new QualifierRangeFilter(family);
+ }
+ }
+
+ /** Matches only cells from columns within the given range. */
+ public static final class QualifierRangeFilter
+ extends AbstractByteStringRange implements Filter {
+ private final String family;
+
+ private QualifierRangeFilter(String family) {
+ super();
+ this.family = family;
+ }
+
+ private QualifierRangeFilter(
+ String family, BoundType startBound, ByteString start, BoundType endBound, ByteString end) {
+ super(startBound, start, endBound, end);
+ this.family = Preconditions.checkNotNull(family);
+ }
+
+ @Override
+ protected QualifierRangeFilter newInstance(
+ BoundType startBound, ByteString start, BoundType endBound, ByteString end) {
+ return new QualifierRangeFilter(family, startBound, start, endBound, end);
+ }
+
+ @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());
+ }
+
+ return RowFilter.newBuilder().setColumnRangeFilter(builder.build()).build();
+ }
+
+ @Override
+ public QualifierRangeFilter clone() {
+ try {
+ return (QualifierRangeFilter) super.clone();
+ } catch (CloneNotSupportedException | ClassCastException e) {
+ throw new RuntimeException("should never happen");
+ }
+ }
+ }
+
+ 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 microsecond timestamps within the given range. Start is inclusive and
+ * end is exclusive.
+ */
+ public static final class TimestampRangeFilter
+ extends AbstractTimestampRange implements Filter {
+ private TimestampRangeFilter() {
+ super();
+ }
+
+ private TimestampRangeFilter(BoundType startBound, Long start, BoundType endBound, Long end) {
+ super(startBound, start, endBound, end);
+ }
+
+ @Override
+ protected TimestampRangeFilter newInstance(
+ BoundType startBound, Long start, BoundType endBound, Long end) {
+ return new TimestampRangeFilter(startBound, start, endBound, end);
+ }
+
+ @InternalApi
+ @Override
+ public RowFilter toProto() {
+ 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() {
+ try {
+ return (TimestampRangeFilter) super.clone();
+ } catch (CloneNotSupportedException | ClassCastException e) {
+ throw new RuntimeException("should never happen");
+ }
+ }
+ }
+
+ public static final class ValueFilter {
+ private ValueFilter() {}
+
+ /**
+ * Matches only cells with values that satisfy the given RE2 regex. 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.
+ */
+ public Filter regex(@Nonnull String regex) {
+ return regex(wrapString(regex));
+ }
+
+ /** Matches only cells with values that match the given value. */
+ public Filter exactMatch(@Nonnull ByteString value) {
+ return regex(RegexUtil.literalRegex(value));
+ }
+
+ /**
+ * Matches only cells with values that satisfy the given RE2 regex. 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.
+ */
+ 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}.
+ *
+ * @return a new {@link ValueRangeFilter}
+ */
+ public ValueRangeFilter range() {
+ return new ValueRangeFilter();
+ }
+
+ /** Replaces each cell's value with the empty string. */
+ public Filter strip() {
+ return STRIP_VALUE;
+ }
+ }
+
+ /** Matches only cells with values that fall within the given value range. */
+ public static final class ValueRangeFilter extends AbstractByteStringRange
+ implements Filter {
+ private ValueRangeFilter() {
+ super();
+ }
+
+ private ValueRangeFilter(
+ BoundType startBound, ByteString start, BoundType endBound, ByteString end) {
+ super(startBound, start, endBound, end);
+ }
+
+ @Override
+ protected ValueRangeFilter newInstance(
+ BoundType startBound, ByteString start, BoundType endBound, ByteString end) {
+ return new ValueRangeFilter(startBound, start, endBound, end);
+ }
+
+ @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();
+ }
+
+ @Override
+ public ValueRangeFilter clone() {
+ try {
+ return (ValueRangeFilter) super.clone();
+ } catch (CloneNotSupportedException | ClassCastException e) {
+ throw new RuntimeException("should never happen");
+ }
+ }
+ }
+
+ 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(@Nonnull RowFilter proto) {
+ Preconditions.checkNotNull(proto);
+ this.proto = proto;
+ }
+
+ @InternalApi
+ @Override
+ public RowFilter toProto() {
+ return proto;
+ }
+
+ @Override
+ public SimpleFilter clone() {
+ try {
+ return (SimpleFilter) super.clone();
+ } catch (CloneNotSupportedException e) {
+ throw new RuntimeException("should never happen", e);
+ }
+ }
+ }
+
+ public interface Filter extends Cloneable {
+ @InternalApi
+ RowFilter toProto();
+ }
+
+ private static ByteString wrapString(@Nullable String value) {
+ if (value == null) {
+ return null;
+ }
+ return ByteString.copyFromUtf8(value);
+ }
+}
diff --git a/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/wrappers/Query.java b/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/wrappers/Query.java
new file mode 100644
index 000000000000..2e5586b6d436
--- /dev/null
+++ b/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/wrappers/Query.java
@@ -0,0 +1,166 @@
+/*
+ * 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.core.InternalApi;
+import com.google.bigtable.v2.ReadRowsRequest;
+import com.google.bigtable.v2.RowRange;
+import com.google.bigtable.v2.TableName;
+import com.google.cloud.bigtable.data.v2.internal.RequestContext;
+import com.google.cloud.bigtable.data.v2.wrappers.Range.ByteStringRange;
+import com.google.common.base.Preconditions;
+import com.google.protobuf.ByteString;
+
+/** A simple wrapper to construct a query for the ReadRows RPC. */
+public class Query {
+ private final String tableId;
+ private final ReadRowsRequest.Builder builder = ReadRowsRequest.newBuilder();
+
+ /**
+ * Constructs a new Query object for the specified table id. The table id will be combined with
+ * the instance name specified in the {@link
+ * com.google.cloud.bigtable.data.v2.BigtableDataSettings}.
+ */
+ public static Query create(String tableId) {
+ return new Query(tableId);
+ }
+
+ private Query(String tableId) {
+ this.tableId = tableId;
+ }
+
+ /** Adds a key to looked up */
+ public Query rowKey(String key) {
+ Preconditions.checkNotNull(key, "Key can't be null.");
+ return rowKey(ByteString.copyFromUtf8(key));
+ }
+
+ /** Adds a key to looked up */
+ public Query rowKey(ByteString key) {
+ Preconditions.checkNotNull(key, "Key can't be null.");
+ builder.getRowsBuilder().addRowKeys(key);
+
+ return this;
+ }
+
+ /**
+ * Adds a range to be looked up.
+ *
+ * @param start The beginning of the range (inclusive). Can be null to represent negative
+ * infinity.
+ * @param end The end of the range (exclusive). Can be null to represent positive infinity.
+ */
+ public Query range(String start, String end) {
+ return range(wrapKey(start), wrapKey(end));
+ }
+
+ /**
+ * Adds a range to be looked up.
+ *
+ * @param start The beginning of the range (inclusive). Can be null to represent negative
+ * infinity.
+ * @param end The end of the range (exclusive). Can be null to represent positive infinity.
+ */
+ public Query range(ByteString start, ByteString end) {
+ RowRange.Builder rangeBuilder = RowRange.newBuilder();
+ if (start != null) {
+ rangeBuilder.setStartKeyClosed(start);
+ }
+ if (end != null) {
+ rangeBuilder.setEndKeyOpen(end);
+ }
+ builder.getRowsBuilder().addRowRanges(rangeBuilder.build());
+ return this;
+ }
+
+ /** Adds a range to be looked up. */
+ public Query range(ByteStringRange range) {
+ RowRange.Builder rangeBuilder = RowRange.newBuilder();
+
+ switch (range.getStartBound()) {
+ case OPEN:
+ rangeBuilder.setStartKeyOpen(range.getStart());
+ break;
+ case CLOSED:
+ rangeBuilder.setStartKeyClosed(range.getStart());
+ break;
+ case UNBOUNDED:
+ rangeBuilder.clearStartKey();
+ break;
+ default:
+ throw new IllegalStateException("Unknown start bound: " + range.getStartBound());
+ }
+
+ switch (range.getEndBound()) {
+ case OPEN:
+ rangeBuilder.setEndKeyOpen(range.getEnd());
+ break;
+ case CLOSED:
+ rangeBuilder.setEndKeyClosed(range.getEnd());
+ break;
+ case UNBOUNDED:
+ rangeBuilder.clearEndKey();
+ break;
+ default:
+ throw new IllegalStateException("Unknown end bound: " + range.getEndBound());
+ }
+
+ builder.getRowsBuilder().addRowRanges(rangeBuilder.build());
+
+ return this;
+ }
+
+ /**
+ * Sets the filter to apply to each row. Only one filter can be set at a time. To use multiple
+ * filters, please use {@link Filters#interleave()} or {@link Filters#chain()}.
+ */
+ public Query filter(Filters.Filter filter) {
+ builder.setFilter(filter.toProto());
+ return this;
+ }
+
+ /** Limits the number of rows that can be returned */
+ public Query limit(long limit) {
+ Preconditions.checkArgument(limit > 0, "Limit must be greater than 0.");
+ builder.setRowsLimit(limit);
+ return this;
+ }
+
+ /**
+ * Creates the request protobuf. This method is considered an internal implementation detail and
+ * not meant to be used by applications.
+ */
+ @InternalApi
+ public ReadRowsRequest toProto(RequestContext requestContext) {
+ TableName tableName =
+ TableName.of(
+ requestContext.getInstanceName().getProject(),
+ requestContext.getInstanceName().getInstance(),
+ tableId);
+
+ return builder
+ .setTableName(tableName.toString())
+ .setAppProfileId(requestContext.getAppProfileId())
+ .build();
+ }
+
+ private static ByteString wrapKey(String key) {
+ if (key == null) {
+ return null;
+ }
+ return ByteString.copyFromUtf8(key);
+ }
+}
diff --git a/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/wrappers/Range.java b/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/wrappers/Range.java
new file mode 100644
index 000000000000..4062851f7616
--- /dev/null
+++ b/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/wrappers/Range.java
@@ -0,0 +1,252 @@
+/*
+ * 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.
+ *
+ * This base class represents the API for all ranges in the Cloud Bigtable client. It is an
+ * immutable class, so all of its modification methods return ne instances. It's intended to support
+ * fluent DSLs. For example:
+ *
+ *
{@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");
+ * }
+ */
+abstract class Range> {
+ public enum BoundType {
+ OPEN,
+ CLOSED,
+ UNBOUNDED
+ }
+
+ final BoundType startBound;
+ final T start;
+ final BoundType endBound;
+ final 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 newInstanceSafe(BoundType.CLOSED, startClosed, BoundType.OPEN, endOpen);
+ }
+
+ /** Creates a new {@link Range} with an unbounded start and the current end. */
+ public R startUnbounded() {
+ return newInstanceSafe(BoundType.UNBOUNDED, null, endBound, end);
+ }
+
+ /** Creates a new {@link Range} with the specified exclusive start and the current end. */
+ public R startOpen(@Nonnull T start) {
+ return newInstanceSafe(BoundType.OPEN, start, endBound, end);
+ }
+
+ /** Creates a new {@link Range} with the specified inclusive start and the current end. */
+ public R startClosed(@Nonnull T start) {
+ return newInstanceSafe(BoundType.CLOSED, start, endBound, end);
+ }
+
+ /** Creates a new {@link Range} with the current start and an unbounded end. */
+ public R endUnbounded() {
+ return newInstanceSafe(startBound, start, BoundType.UNBOUNDED, null);
+ }
+
+ /** Creates a new {@link Range} with the specified exclusive end and the current start. */
+ public R endOpen(@Nonnull T end) {
+ return newInstanceSafe(startBound, start, BoundType.OPEN, end);
+ }
+
+ /** Creates a new {@link Range} with the specified inclusive end and the current start. */
+ public R endClosed(@Nonnull T end) {
+ return newInstanceSafe(startBound, start, BoundType.CLOSED, end);
+ }
+
+ /** 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;
+ }
+
+ R newInstanceSafe(BoundType startBound, T start, BoundType endBound, T end) {
+ if (startBound != BoundType.UNBOUNDED) {
+ Preconditions.checkNotNull(start, "Bounded start can't be null.");
+ }
+ if (endBound != BoundType.UNBOUNDED) {
+ Preconditions.checkNotNull(end, "Bounded end can't be null");
+ }
+ return newInstance(startBound, start, endBound, end);
+ }
+ /**
+ * Extension point for subclasses to override. This allows subclasses to maintain chainability.
+ */
+ abstract R newInstance(BoundType startBound, T start, BoundType endBound, T end);
+
+ /** Abstract specialization of a {@link Range} for timestamps. */
+ abstract static class AbstractTimestampRange>
+ extends Range {
+ AbstractTimestampRange() {
+ super();
+ }
+
+ AbstractTimestampRange(BoundType startBound, Long start, BoundType endBound, Long end) {
+ super(startBound, start, endBound, end);
+ }
+ }
+
+ /**
+ * Abstract specialization of a {@link Range} for {@link ByteString}s. Allows for easy interop
+ * with simple Strings.
+ */
+ abstract static class AbstractByteStringRange>
+ extends Range {
+ 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 newInstanceSafe(BoundType.CLOSED, wrap(startClosed), BoundType.OPEN, wrap(endOpen));
+ }
+
+ /** Creates a new {@link Range} with the specified exclusive start and the current end. */
+ public R startOpen(String start) {
+ return newInstanceSafe(BoundType.OPEN, wrap(start), endBound, end);
+ }
+
+ /** Creates a new {@link Range} with the specified inclusive start and the current end. */
+ public R startClosed(String start) {
+ return newInstanceSafe(BoundType.CLOSED, wrap(start), endBound, end);
+ }
+
+ /** Creates a new {@link Range} with the specified exclusive end and the current start. */
+ public R endOpen(String end) {
+ return newInstanceSafe(startBound, start, BoundType.OPEN, wrap(end));
+ }
+
+ /** Creates a new {@link Range} with the specified inclusive end and the current start. */
+ public R endClosed(String end) {
+ return newInstanceSafe(startBound, start, BoundType.CLOSED, wrap(end));
+ }
+
+ static ByteString wrap(String str) {
+ return ByteString.copyFromUtf8(str);
+ }
+ }
+
+ /** Concrete Range for timestamps */
+ public static final class TimestampRange extends AbstractTimestampRange {
+ 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);
+ }
+
+ @Override
+ TimestampRange newInstance(BoundType startBound, Long start, BoundType endBound, Long end) {
+ return new TimestampRange(startBound, start, endBound, end);
+ }
+ }
+
+ /** Concrete Range for ByteStrings */
+ public static final class ByteStringRange extends AbstractByteStringRange {
+ 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);
+ }
+
+ @Override
+ ByteStringRange newInstance(
+ BoundType startBound, ByteString start, BoundType endBound, ByteString end) {
+ return new ByteStringRange(startBound, start, endBound, end);
+ }
+ }
+}
diff --git a/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/wrappers/Row.java b/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/wrappers/Row.java
new file mode 100644
index 000000000000..4956759a6f5e
--- /dev/null
+++ b/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/wrappers/Row.java
@@ -0,0 +1,66 @@
+/*
+ * 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.core.BetaApi;
+import com.google.api.core.InternalApi;
+import com.google.auto.value.AutoValue;
+import com.google.protobuf.ByteString;
+import java.util.List;
+import javax.annotation.Nonnull;
+
+/** Default representation of a logical row. */
+@BetaApi
+@AutoValue
+public abstract class Row implements Comparable {
+ /** Creates a new instance of the {@link Row}. */
+ @InternalApi
+ public static Row create(ByteString key, List cells) {
+ return new AutoValue_Row(key, cells);
+ }
+
+ /** Returns the row key */
+ @Nonnull
+ public abstract ByteString getKey();
+
+ /**
+ * Returns the list of cells. The cells will be clustered by their family and sorted by their
+ * qualifier.
+ */
+ public abstract List getCells();
+
+ /** Lexicographically compares this row's key to another row's key. */
+ @Override
+ public int compareTo(@Nonnull Row row) {
+ int sizeA = getKey().size();
+ int sizeB = row.getKey().size();
+ int size = Math.min(sizeA, sizeB);
+
+ for (int i = 0; i < size; i++) {
+ int byteA = getKey().byteAt(i) & 0xff;
+ int byteB = row.getKey().byteAt(i) & 0xff;
+ if (byteA == byteB) {
+ continue;
+ } else {
+ return byteA < byteB ? -1 : 1;
+ }
+ }
+ if (sizeA == sizeB) {
+ return 0;
+ }
+ return sizeA < sizeB ? -1 : 1;
+ }
+}
diff --git a/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/wrappers/RowAdapter.java b/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/wrappers/RowAdapter.java
new file mode 100644
index 000000000000..5c9692fcfa6a
--- /dev/null
+++ b/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/wrappers/RowAdapter.java
@@ -0,0 +1,81 @@
+/*
+ * 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.protobuf.ByteString;
+import java.util.List;
+
+/**
+ * An extension point that allows end users to plug in a custom implementation of logical rows. This
+ * useful in cases where the user would like to apply advanced client side filtering of cells. This
+ * adapter acts like a factory for a SAX style row builder.
+ */
+public interface RowAdapter {
+ /** Creates a new instance of a {@link RowBuilder}. */
+ RowBuilder createRowBuilder();
+
+ /**
+ * Checks if the given row is a special marker row. Please the documentation for {@link
+ * RowBuilder} for more information
+ */
+ boolean isScanMarkerRow(RowT row);
+
+ ByteString getKey(RowT row);
+
+ /**
+ * A SAX style row factory. It is responsible for creating two types of rows: standard data rows
+ * and special marker rows. Marker rows are emitted when skipping lots of rows due to filters. The
+ * server notifies the client of the last row it skipped to help client resume in case of error.
+ *
+ * State management is handled external to the implementation of this class and guarantees the
+ * following order:
+ *
+ *
+ * - Exactly 1 {@code startRow} for each row.
+ *
- Exactly 1 {@code startCell} for each cell.
+ *
- At least 1 {@code cellValue} for each cell.
+ *
- Exactly 1 {@code finishCell} for each cell.
+ *
- Exactly 1 {@code finishRow} for each row.
+ *
+ *
+ * {@code createScanMarkerRow} can be called one or more times between {@code finishRow} and
+ * {@code startRow}. {@code reset} can be called at any point and can be invoked multiple times in
+ * a row.
+ */
+ interface RowBuilder {
+ /** Called to start a new row. This will be called once per row. */
+ void startRow(ByteString key);
+
+ /** Called to start a new cell in a row. */
+ void startCell(
+ String family, ByteString qualifier, long timestamp, List labels, long size);
+
+ /** Called multiple times per cell to concatenate the cell value. */
+ void cellValue(ByteString value);
+
+ /** Called once per cell to signal the end of the value (unless reset). */
+ void finishCell();
+
+ /** Called once per row to signal that all cells have been processed (unless reset). */
+ RowT finishRow();
+
+ /** Called when the current in progress row should be dropped */
+ void reset();
+
+ /** Creates a special row to mark server progress before any data is received */
+ RowT createScanMarkerRow(ByteString key);
+ }
+}
diff --git a/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/wrappers/RowCell.java b/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/wrappers/RowCell.java
new file mode 100644
index 000000000000..e90466d00f46
--- /dev/null
+++ b/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/wrappers/RowCell.java
@@ -0,0 +1,57 @@
+/*
+ * 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.core.InternalApi;
+import com.google.auto.value.AutoValue;
+import com.google.protobuf.ByteString;
+import java.util.List;
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+
+/** Default representation of a cell in a {@link Row}. */
+@AutoValue
+public abstract class RowCell {
+ /** Creates a new instance of the {@link RowCell}. */
+ @InternalApi
+ public static RowCell create(
+ String family, ByteString qualifier, long timestamp, List labels, ByteString value) {
+ return new AutoValue_RowCell(family, qualifier, timestamp, value, labels);
+ }
+
+ /** The cell's family */
+ @Nonnull
+ public abstract String family();
+
+ /** The cell's qualifier (column name) */
+ @Nullable
+ public abstract ByteString qualifier();
+
+ /** The timestamp of the cell */
+ public abstract long timestamp();
+
+ /** The value of the cell */
+ @Nonnull
+ public abstract ByteString value();
+
+ /**
+ * The labels assigned to the cell
+ *
+ * @see Filters#label(String)
+ */
+ @Nonnull
+ public abstract List labels();
+}
diff --git a/google-cloud-bigtable/src/test/java/com/google/cloud/bigtable/data/v2/BigtableDataClientTest.java b/google-cloud-bigtable/src/test/java/com/google/cloud/bigtable/data/v2/BigtableDataClientTest.java
index 5c0e80dc2ad6..411ca1b86ac5 100644
--- a/google-cloud-bigtable/src/test/java/com/google/cloud/bigtable/data/v2/BigtableDataClientTest.java
+++ b/google-cloud-bigtable/src/test/java/com/google/cloud/bigtable/data/v2/BigtableDataClientTest.java
@@ -15,7 +15,13 @@
*/
package com.google.cloud.bigtable.data.v2;
+import static com.google.common.truth.Truth.assertThat;
+
+import com.google.api.gax.rpc.ResponseObserver;
+import com.google.api.gax.rpc.ServerStreamingCallable;
import com.google.cloud.bigtable.data.v2.stub.EnhancedBigtableStub;
+import com.google.cloud.bigtable.data.v2.wrappers.Query;
+import com.google.cloud.bigtable.data.v2.wrappers.Row;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
@@ -26,11 +32,13 @@
@RunWith(MockitoJUnitRunner.class)
public class BigtableDataClientTest {
@Mock private EnhancedBigtableStub mockStub;
+ @Mock private ServerStreamingCallable mockReadRowsCallable;
private BigtableDataClient bigtableDataClient;
@Before
public void setUp() {
+ Mockito.when(mockStub.readRowsCallable()).thenReturn(mockReadRowsCallable);
bigtableDataClient = new BigtableDataClient(mockStub);
}
@@ -39,4 +47,27 @@ public void proxyCloseTest() throws Exception {
bigtableDataClient.close();
Mockito.verify(mockStub).close();
}
+
+ @Test
+ public void proxyReadRowsCallableTest() {
+ assertThat(bigtableDataClient.readRowsCallable()).isSameAs(mockReadRowsCallable);
+ }
+
+ @Test
+ public void proxyReadRowsSyncTest() {
+ Query query = Query.create("fake-table");
+ bigtableDataClient.readRows(query);
+
+ Mockito.verify(mockReadRowsCallable).call(query);
+ }
+
+ @Test
+ public void proxyReadRowsAsyncTest() {
+ Query query = Query.create("fake-table");
+ @SuppressWarnings("unchecked")
+ ResponseObserver mockObserver = Mockito.mock(ResponseObserver.class);
+ bigtableDataClient.readRowsAsync(query, mockObserver);
+
+ Mockito.verify(mockReadRowsCallable).call(query, mockObserver);
+ }
}
diff --git a/google-cloud-bigtable/src/test/java/com/google/cloud/bigtable/data/v2/internal/RegexUtilTest.java b/google-cloud-bigtable/src/test/java/com/google/cloud/bigtable/data/v2/internal/RegexUtilTest.java
new file mode 100644
index 000000000000..befd4281d295
--- /dev/null
+++ b/google-cloud-bigtable/src/test/java/com/google/cloud/bigtable/data/v2/internal/RegexUtilTest.java
@@ -0,0 +1,51 @@
+/*
+ * 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 static com.google.common.truth.Truth.assertThat;
+
+import com.google.protobuf.ByteString;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+@RunWith(JUnit4.class)
+public class RegexUtilTest {
+ @Test
+ public void literalRegexPassthroughTest() {
+ ByteString input = ByteString.copyFromUtf8("hi");
+ ByteString actual = RegexUtil.literalRegex(input);
+
+ assertThat(actual).isEqualTo(input);
+ }
+
+ @Test
+ public void literalRegexEscapeTest() {
+ ByteString input = ByteString.copyFromUtf8("h.*i");
+ ByteString actual = RegexUtil.literalRegex(input);
+ ByteString expected = ByteString.copyFromUtf8("h\\.\\*i");
+
+ assertThat(actual).isEqualTo(expected);
+ }
+
+ @Test
+ public void literalRegexEscapeBytes() {
+ ByteString input = ByteString.copyFrom(new byte[] {(byte) 0xe2, (byte) 0x80, (byte) 0xb3});
+ ByteString actual = RegexUtil.literalRegex(input);
+
+ assertThat(actual).isEqualTo(input);
+ }
+}
diff --git a/google-cloud-bigtable/src/test/java/com/google/cloud/bigtable/data/v2/stub/EnhancedBigtableStubSettingsTest.java b/google-cloud-bigtable/src/test/java/com/google/cloud/bigtable/data/v2/stub/EnhancedBigtableStubSettingsTest.java
index 7bdb83d4a172..ce76d5e8052d 100644
--- a/google-cloud-bigtable/src/test/java/com/google/cloud/bigtable/data/v2/stub/EnhancedBigtableStubSettingsTest.java
+++ b/google-cloud-bigtable/src/test/java/com/google/cloud/bigtable/data/v2/stub/EnhancedBigtableStubSettingsTest.java
@@ -19,12 +19,19 @@
import com.google.api.gax.core.CredentialsProvider;
import com.google.api.gax.grpc.InstantiatingGrpcChannelProvider;
+import com.google.api.gax.retrying.RetrySettings;
+import com.google.api.gax.rpc.ServerStreamingCallSettings;
+import com.google.api.gax.rpc.StatusCode.Code;
import com.google.bigtable.admin.v2.InstanceName;
import com.google.cloud.bigtable.data.v2.BigtableDataSettings;
+import com.google.cloud.bigtable.data.v2.wrappers.Query;
+import com.google.cloud.bigtable.data.v2.wrappers.Row;
+import java.io.IOException;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.JUnit4;
import org.mockito.Mockito;
+import org.threeten.bp.Duration;
@RunWith(JUnit4.class)
public class EnhancedBigtableStubSettingsTest {
@@ -98,4 +105,77 @@ public void multipleChannelsByDefaultTest() {
assertThat(provider.toBuilder().getPoolSize()).isGreaterThan(1);
}
+
+ @Test
+ public void readRowsIsNotLostTest() throws IOException {
+ InstanceName dummyInstanceName = InstanceName.of("my-project", "my-instance");
+
+ EnhancedBigtableStubSettings.Builder builder =
+ EnhancedBigtableStubSettings.newBuilder().setInstanceName(dummyInstanceName);
+
+ RetrySettings retrySettings =
+ RetrySettings.newBuilder()
+ .setMaxAttempts(10)
+ .setTotalTimeout(Duration.ofHours(1))
+ .setInitialRpcTimeout(Duration.ofSeconds(10))
+ .setRpcTimeoutMultiplier(1)
+ .setMaxRpcTimeout(Duration.ofSeconds(10))
+ .setJittered(true)
+ .build();
+
+ builder
+ .readRowsSettings()
+ .setTimeoutCheckInterval(Duration.ofSeconds(10))
+ .setIdleTimeout(Duration.ofMinutes(5))
+ .setRetryableCodes(Code.ABORTED, Code.DEADLINE_EXCEEDED)
+ .setRetrySettings(retrySettings)
+ .build();
+
+ assertThat(builder.readRowsSettings().getTimeoutCheckInterval())
+ .isEqualTo(Duration.ofSeconds(10));
+ assertThat(builder.readRowsSettings().getIdleTimeout()).isEqualTo(Duration.ofMinutes(5));
+ assertThat(builder.readRowsSettings().getRetryableCodes())
+ .containsAllOf(Code.ABORTED, Code.DEADLINE_EXCEEDED);
+ assertThat(builder.readRowsSettings().getRetrySettings()).isEqualTo(retrySettings);
+
+ assertThat(builder.build().readRowsSettings().getTimeoutCheckInterval())
+ .isEqualTo(Duration.ofSeconds(10));
+ assertThat(builder.build().readRowsSettings().getIdleTimeout())
+ .isEqualTo(Duration.ofMinutes(5));
+ assertThat(builder.build().readRowsSettings().getRetryableCodes())
+ .containsAllOf(Code.ABORTED, Code.DEADLINE_EXCEEDED);
+ assertThat(builder.build().readRowsSettings().getRetrySettings()).isEqualTo(retrySettings);
+
+ assertThat(builder.build().toBuilder().readRowsSettings().getTimeoutCheckInterval())
+ .isEqualTo(Duration.ofSeconds(10));
+ assertThat(builder.build().toBuilder().readRowsSettings().getIdleTimeout())
+ .isEqualTo(Duration.ofMinutes(5));
+ assertThat(builder.build().toBuilder().readRowsSettings().getRetryableCodes())
+ .containsAllOf(Code.ABORTED, Code.DEADLINE_EXCEEDED);
+ assertThat(builder.build().toBuilder().readRowsSettings().getRetrySettings())
+ .isEqualTo(retrySettings);
+ }
+
+ @Test
+ public void readRowsHasSaneDefaultsTest() {
+ ServerStreamingCallSettings.Builder builder =
+ EnhancedBigtableStubSettings.newBuilder().readRowsSettings();
+
+ assertThat(builder.getTimeoutCheckInterval()).isGreaterThan(Duration.ZERO);
+ assertThat(builder.getIdleTimeout()).isGreaterThan(Duration.ZERO);
+
+ assertThat(builder.getRetryableCodes())
+ .containsAllOf(Code.DEADLINE_EXCEEDED, Code.UNAVAILABLE, Code.ABORTED);
+
+ assertThat(builder.getRetrySettings().getMaxAttempts()).isGreaterThan(1);
+ assertThat(builder.getRetrySettings().getTotalTimeout()).isGreaterThan(Duration.ZERO);
+
+ assertThat(builder.getRetrySettings().getInitialRetryDelay()).isGreaterThan(Duration.ZERO);
+ assertThat(builder.getRetrySettings().getRetryDelayMultiplier()).isAtLeast(1.0);
+ assertThat(builder.getRetrySettings().getMaxRetryDelay()).isGreaterThan(Duration.ZERO);
+
+ assertThat(builder.getRetrySettings().getInitialRpcTimeout()).isGreaterThan(Duration.ZERO);
+ assertThat(builder.getRetrySettings().getRpcTimeoutMultiplier()).isAtLeast(1.0);
+ assertThat(builder.getRetrySettings().getMaxRpcTimeout()).isGreaterThan(Duration.ZERO);
+ }
}
diff --git a/google-cloud-bigtable/src/test/java/com/google/cloud/bigtable/data/v2/wrappers/DefaultRowAdapterTest.java b/google-cloud-bigtable/src/test/java/com/google/cloud/bigtable/data/v2/wrappers/DefaultRowAdapterTest.java
new file mode 100644
index 000000000000..ac576dad7a71
--- /dev/null
+++ b/google-cloud-bigtable/src/test/java/com/google/cloud/bigtable/data/v2/wrappers/DefaultRowAdapterTest.java
@@ -0,0 +1,131 @@
+/*
+ * 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.common.truth.Truth.assertThat;
+
+import com.google.cloud.bigtable.data.v2.wrappers.RowAdapter.RowBuilder;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.Lists;
+import com.google.protobuf.ByteString;
+import java.util.List;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+@RunWith(JUnit4.class)
+public class DefaultRowAdapterTest {
+
+ private final DefaultRowAdapter adapter = new DefaultRowAdapter();
+ private RowBuilder rowBuilder;
+
+ @Before
+ public void setUp() {
+ rowBuilder = adapter.createRowBuilder();
+ }
+
+ @Test
+ public void singleCellRowTest() {
+ ByteString value = ByteString.copyFromUtf8("my-value");
+ rowBuilder.startRow(ByteString.copyFromUtf8("my-key"));
+ rowBuilder.startCell(
+ "my-family",
+ ByteString.copyFromUtf8("my-qualifier"),
+ 100,
+ ImmutableList.of("my-label"),
+ value.size());
+ rowBuilder.cellValue(value);
+ rowBuilder.finishCell();
+
+ assertThat(rowBuilder.finishRow())
+ .isEqualTo(
+ Row.create(
+ ByteString.copyFromUtf8("my-key"),
+ ImmutableList.of(
+ RowCell.create(
+ "my-family",
+ ByteString.copyFromUtf8("my-qualifier"),
+ 100,
+ ImmutableList.of("my-label"),
+ value))));
+ }
+
+ @Test
+ public void multiCellTest() {
+ List expectedCells = Lists.newArrayList();
+
+ rowBuilder.startRow(ByteString.copyFromUtf8("my-key"));
+
+ for (int i = 0; i < 10; i++) {
+ ByteString value = ByteString.copyFromUtf8("value-" + i);
+ ByteString qualifier = ByteString.copyFromUtf8("qualifier-" + i);
+ rowBuilder.startCell("family", qualifier, 1000, ImmutableList.of("my-label"), value.size());
+ rowBuilder.cellValue(value);
+ rowBuilder.finishCell();
+
+ expectedCells.add(
+ RowCell.create("family", qualifier, 1000, ImmutableList.of("my-label"), value));
+ }
+
+ assertThat(rowBuilder.finishRow())
+ .isEqualTo(Row.create(ByteString.copyFromUtf8("my-key"), expectedCells));
+ }
+
+ @Test
+ public void splitCellTest() {
+ ByteString part1 = ByteString.copyFromUtf8("part1");
+ ByteString part2 = ByteString.copyFromUtf8("part2");
+
+ rowBuilder.startRow(ByteString.copyFromUtf8("my-key"));
+ rowBuilder.startCell(
+ "family",
+ ByteString.copyFromUtf8("qualifier"),
+ 1000,
+ ImmutableList.of("my-label"),
+ part1.size() + part2.size());
+ rowBuilder.cellValue(part1);
+ rowBuilder.cellValue(part2);
+ rowBuilder.finishCell();
+
+ assertThat(rowBuilder.finishRow())
+ .isEqualTo(
+ Row.create(
+ ByteString.copyFromUtf8("my-key"),
+ ImmutableList.of(
+ RowCell.create(
+ "family",
+ ByteString.copyFromUtf8("qualifier"),
+ 1000,
+ ImmutableList.of("my-label"),
+ ByteString.copyFromUtf8("part1part2")))));
+ }
+
+ @Test
+ public void markerRowTest() {
+ Row markerRow = rowBuilder.createScanMarkerRow(ByteString.copyFromUtf8("key"));
+ assertThat(adapter.isScanMarkerRow(markerRow)).isTrue();
+
+ ByteString value = ByteString.copyFromUtf8("value");
+ rowBuilder.startRow(ByteString.copyFromUtf8("key"));
+ rowBuilder.startCell(
+ "family", ByteString.EMPTY, 1000, ImmutableList.of(), value.size());
+ rowBuilder.cellValue(value);
+ rowBuilder.finishCell();
+
+ assertThat(adapter.isScanMarkerRow(rowBuilder.finishRow())).isFalse();
+ }
+}
diff --git a/google-cloud-bigtable/src/test/java/com/google/cloud/bigtable/data/v2/wrappers/FiltersTest.java b/google-cloud-bigtable/src/test/java/com/google/cloud/bigtable/data/v2/wrappers/FiltersTest.java
new file mode 100644
index 000000000000..1aa66f9200f2
--- /dev/null
+++ b/google-cloud-bigtable/src/test/java/com/google/cloud/bigtable/data/v2/wrappers/FiltersTest.java
@@ -0,0 +1,436 @@
+/*
+ * 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.FILTERS;
+import static com.google.common.truth.Truth.assertThat;
+
+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.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+@RunWith(JUnit4.class)
+public class FiltersTest {
+ @Test
+ public void chainTest() {
+ RowFilter actualProto =
+ FILTERS
+ .chain()
+ .filter(FILTERS.key().regex(".*"))
+ .filter(FILTERS.key().sample(0.5))
+ .filter(FILTERS.chain().filter(FILTERS.family().regex("hi$")).filter(FILTERS.pass()))
+ .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().setPassAllFilter(true)))))
+ .build();
+
+ assertThat(actualProto).isEqualTo(expectedFilter);
+ }
+
+ @Test
+ public void interleaveTest() {
+ RowFilter actualProto =
+ FILTERS
+ .interleave()
+ .filter(FILTERS.key().regex(".*"))
+ .filter(FILTERS.key().sample(0.5))
+ .filter(
+ FILTERS.interleave().filter(FILTERS.family().regex("hi$")).filter(FILTERS.pass()))
+ .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().setPassAllFilter(true).build()))))
+ .build();
+
+ assertThat(actualProto).isEqualTo(expectedFilter);
+ }
+
+ @Test
+ public void conditionTest() {
+ RowFilter actualFilter =
+ FILTERS
+ .condition(FILTERS.key().regex(".*"))
+ .then(FILTERS.label("true"))
+ .otherwise(FILTERS.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();
+
+ assertThat(actualFilter).isEqualTo(expectedFilter);
+ }
+
+ @Test
+ public void keyRegexTest() {
+ RowFilter actualFilter = FILTERS.key().regex(ByteString.copyFromUtf8(".*")).toProto();
+
+ RowFilter expectedFilter =
+ RowFilter.newBuilder().setRowKeyRegexFilter(ByteString.copyFromUtf8(".*")).build();
+
+ assertThat(actualFilter).isEqualTo(expectedFilter);
+ }
+
+ @Test
+ public void keyRegexStringTest() {
+ RowFilter actualFilter = FILTERS.key().regex(".*").toProto();
+
+ RowFilter expectedFilter =
+ RowFilter.newBuilder().setRowKeyRegexFilter(ByteString.copyFromUtf8(".*")).build();
+
+ assertThat(actualFilter).isEqualTo(expectedFilter);
+ }
+
+ @Test
+ public void keyExactMatchTest() {
+ RowFilter actualFilter = FILTERS.key().exactMatch(ByteString.copyFromUtf8(".*")).toProto();
+
+ RowFilter expectedFilter =
+ RowFilter.newBuilder().setRowKeyRegexFilter(ByteString.copyFromUtf8("\\.\\*")).build();
+
+ assertThat(actualFilter).isEqualTo(expectedFilter);
+ }
+
+ @Test
+ public void keySampleTest() {
+ RowFilter actualFilter = FILTERS.key().sample(0.3).toProto();
+
+ RowFilter expectedFilter = RowFilter.newBuilder().setRowSampleFilter(0.3).build();
+
+ assertThat(actualFilter).isEqualTo(expectedFilter);
+ }
+
+ @Test
+ public void familyRegexTest() {
+ RowFilter actualFilter = FILTERS.family().regex("^hi").toProto();
+
+ RowFilter expectedFilter = RowFilter.newBuilder().setFamilyNameRegexFilter("^hi").build();
+
+ assertThat(actualFilter).isEqualTo(expectedFilter);
+ }
+
+ @Test
+ public void familyExactMatchTest() {
+ RowFilter actualFilter = FILTERS.family().exactMatch("^hi").toProto();
+
+ RowFilter expectedFilter = RowFilter.newBuilder().setFamilyNameRegexFilter("\\^hi").build();
+
+ assertThat(actualFilter).isEqualTo(expectedFilter);
+ }
+
+ @Test
+ public void qualifierRegexTest() {
+ RowFilter actualFilter = FILTERS.qualifier().regex(ByteString.copyFromUtf8("^hi")).toProto();
+
+ RowFilter expectedFilter =
+ RowFilter.newBuilder()
+ .setColumnQualifierRegexFilter(ByteString.copyFromUtf8("^hi"))
+ .build();
+
+ assertThat(actualFilter).isEqualTo(expectedFilter);
+ }
+
+ @Test
+ public void qualifierRegexStringTest() {
+ RowFilter actualFilter = FILTERS.qualifier().regex("^hi").toProto();
+
+ RowFilter expectedFilter =
+ RowFilter.newBuilder()
+ .setColumnQualifierRegexFilter(ByteString.copyFromUtf8("^hi"))
+ .build();
+
+ assertThat(actualFilter).isEqualTo(expectedFilter);
+ }
+
+ @Test
+ public void qualifierExactMatchTest() {
+ RowFilter actualFilter =
+ FILTERS.qualifier().exactMatch(ByteString.copyFromUtf8("^hi")).toProto();
+
+ RowFilter expectedFilter =
+ RowFilter.newBuilder()
+ .setColumnQualifierRegexFilter(ByteString.copyFromUtf8("\\^hi"))
+ .build();
+
+ assertThat(actualFilter).isEqualTo(expectedFilter);
+ }
+
+ @Test
+ public void qualifierRangeInFamilyClosedOpen() {
+ 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();
+
+ assertThat(actualFilter).isEqualTo(expectedFilter);
+ }
+
+ @Test
+ public void qualifierRangeInFamilyOpenClosed() {
+ RowFilter actualFilter =
+ FILTERS
+ .qualifier()
+ .rangeWithinFamily("family")
+ .startOpen("begin")
+ .endClosed("end")
+ .toProto();
+
+ RowFilter expectedFilter =
+ RowFilter.newBuilder()
+ .setColumnRangeFilter(
+ ColumnRange.newBuilder()
+ .setFamilyName("family")
+ .setStartQualifierOpen(ByteString.copyFromUtf8("begin"))
+ .setEndQualifierClosed(ByteString.copyFromUtf8("end")))
+ .build();
+
+ assertThat(actualFilter).isEqualTo(expectedFilter);
+ }
+
+ @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();
+
+ assertThat(actualFilter).isEqualTo(expectedFilter);
+ }
+
+ @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();
+
+ assertThat(actualFilter).isEqualTo(expectedFilter);
+ }
+
+ @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();
+
+ assertThat(actualFilter).isEqualTo(expectedFilter);
+ }
+
+ @Test
+ public void valueRegex() {
+ RowFilter actualFilter = FILTERS.value().regex("some[0-9]regex").toProto();
+
+ RowFilter expectedFilter =
+ RowFilter.newBuilder()
+ .setValueRegexFilter(ByteString.copyFromUtf8("some[0-9]regex"))
+ .build();
+
+ assertThat(actualFilter).isEqualTo(expectedFilter);
+ }
+
+ @Test
+ public void valueExactMatch() {
+ RowFilter actualFilter =
+ FILTERS.value().exactMatch(ByteString.copyFromUtf8("some[0-9]regex")).toProto();
+
+ RowFilter expectedFilter =
+ RowFilter.newBuilder()
+ .setValueRegexFilter(ByteString.copyFromUtf8("some\\[0\\-9\\]regex"))
+ .build();
+
+ assertThat(actualFilter).isEqualTo(expectedFilter);
+ }
+
+ @Test
+ public void valueRangeClosedOpen() {
+ RowFilter actualFilter = FILTERS.value().range().startClosed("begin").endOpen("end").toProto();
+
+ RowFilter expectedFilter =
+ RowFilter.newBuilder()
+ .setValueRangeFilter(
+ ValueRange.newBuilder()
+ .setStartValueClosed(ByteString.copyFromUtf8("begin"))
+ .setEndValueOpen(ByteString.copyFromUtf8("end")))
+ .build();
+
+ assertThat(actualFilter).isEqualTo(expectedFilter);
+ }
+
+ @Test
+ public void valueRangeOpenClosed() {
+ RowFilter actualFilter = FILTERS.value().range().startOpen("begin").endClosed("end").toProto();
+
+ RowFilter expectedFilter =
+ RowFilter.newBuilder()
+ .setValueRangeFilter(
+ ValueRange.newBuilder()
+ .setStartValueOpen(ByteString.copyFromUtf8("begin"))
+ .setEndValueClosed(ByteString.copyFromUtf8("end")))
+ .build();
+
+ assertThat(actualFilter).isEqualTo(expectedFilter);
+ }
+
+ @Test
+ public void valueStripTest() {
+ RowFilter actualFilter = FILTERS.value().strip().toProto();
+
+ RowFilter expectedFilter = RowFilter.newBuilder().setStripValueTransformer(true).build();
+
+ assertThat(actualFilter).isEqualTo(expectedFilter);
+ }
+
+ @Test
+ public void offsetCellsPerRowTest() {
+ RowFilter actualFilter = FILTERS.offset().cellsPerRow(10).toProto();
+
+ RowFilter expectedFilter = RowFilter.newBuilder().setCellsPerRowOffsetFilter(10).build();
+
+ assertThat(actualFilter).isEqualTo(expectedFilter);
+ }
+
+ @Test
+ public void limitCellsPerRowTest() {
+ RowFilter actualFilter = FILTERS.limit().cellsPerRow(10).toProto();
+
+ RowFilter expectedFilter = RowFilter.newBuilder().setCellsPerRowLimitFilter(10).build();
+
+ assertThat(actualFilter).isEqualTo(expectedFilter);
+ }
+
+ @Test
+ public void limitCellsPerColumnTest() {
+ RowFilter actualFilter = FILTERS.limit().cellsPerColumn(10).toProto();
+
+ RowFilter expectedFilter = RowFilter.newBuilder().setCellsPerColumnLimitFilter(10).build();
+
+ assertThat(actualFilter).isEqualTo(expectedFilter);
+ }
+
+ @Test
+ public void passTest() {
+ RowFilter actualFilter = FILTERS.pass().toProto();
+
+ RowFilter expectedFilter = RowFilter.newBuilder().setPassAllFilter(true).build();
+
+ assertThat(actualFilter).isEqualTo(expectedFilter);
+ }
+
+ @Test
+ public void blockTest() {
+ RowFilter actualFilter = FILTERS.block().toProto();
+
+ RowFilter expectedFilter = RowFilter.newBuilder().setBlockAllFilter(true).build();
+
+ assertThat(actualFilter).isEqualTo(expectedFilter);
+ }
+
+ @Test
+ public void sinkTest() {
+ RowFilter actualFilter = FILTERS.sink().toProto();
+
+ RowFilter expectedFilter = RowFilter.newBuilder().setSink(true).build();
+
+ assertThat(actualFilter).isEqualTo(expectedFilter);
+ }
+
+ @Test
+ public void labelTest() {
+ RowFilter actualFilter = FILTERS.label("my-label").toProto();
+
+ RowFilter expectedFilter = RowFilter.newBuilder().setApplyLabelTransformer("my-label").build();
+
+ assertThat(actualFilter).isEqualTo(expectedFilter);
+ }
+}
diff --git a/google-cloud-bigtable/src/test/java/com/google/cloud/bigtable/data/v2/wrappers/QueryTest.java b/google-cloud-bigtable/src/test/java/com/google/cloud/bigtable/data/v2/wrappers/QueryTest.java
new file mode 100644
index 000000000000..9e3da96165e7
--- /dev/null
+++ b/google-cloud-bigtable/src/test/java/com/google/cloud/bigtable/data/v2/wrappers/QueryTest.java
@@ -0,0 +1,129 @@
+/*
+ * 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.FILTERS;
+import static com.google.common.truth.Truth.assertThat;
+
+import com.google.bigtable.admin.v2.InstanceName;
+import com.google.bigtable.v2.ReadRowsRequest;
+import com.google.bigtable.v2.ReadRowsRequest.Builder;
+import com.google.bigtable.v2.RowFilter;
+import com.google.bigtable.v2.RowRange;
+import com.google.bigtable.v2.TableName;
+import com.google.cloud.bigtable.data.v2.internal.RequestContext;
+import com.google.cloud.bigtable.data.v2.wrappers.Range.ByteStringRange;
+import com.google.protobuf.ByteString;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+@RunWith(JUnit4.class)
+public class QueryTest {
+ private static final InstanceName INSTANCE_NAME =
+ InstanceName.of("fake-project", "fake-instance");
+ private static final TableName TABLE_NAME =
+ TableName.of("fake-project", "fake-instance", "fake-table");
+ private static final String APP_PROFILE_ID = "fake-profile-id";
+ private RequestContext requestContext;
+
+ @Before
+ public void setUp() {
+ requestContext = RequestContext.create(INSTANCE_NAME, APP_PROFILE_ID);
+ }
+
+ @Test
+ public void requestContextTest() {
+ Query query = Query.create(TABLE_NAME.getTable());
+
+ ReadRowsRequest proto = query.toProto(requestContext);
+ assertThat(proto).isEqualTo(expectedProtoBuilder().build());
+ }
+
+ @Test
+ public void rowKeysTest() {
+ Query query =
+ Query.create(TABLE_NAME.getTable())
+ .rowKey("simple-string")
+ .rowKey(ByteString.copyFromUtf8("byte-string"));
+
+ ReadRowsRequest.Builder expectedProto = expectedProtoBuilder();
+ expectedProto
+ .getRowsBuilder()
+ .addRowKeys(ByteString.copyFromUtf8("simple-string"))
+ .addRowKeys(ByteString.copyFromUtf8("byte-string"));
+
+ ReadRowsRequest actualProto = query.toProto(requestContext);
+ assertThat(actualProto).isEqualTo(expectedProto.build());
+ }
+
+ @Test
+ public void rowRangeTest() {
+ Query query =
+ Query.create(TABLE_NAME.getTable())
+ .range("simple-begin", "simple-end")
+ .range(ByteString.copyFromUtf8("byte-begin"), ByteString.copyFromUtf8("byte-end"))
+ .range(ByteStringRange.create("range-begin", "range-end"));
+
+ Builder expectedProto = expectedProtoBuilder();
+ expectedProto
+ .getRowsBuilder()
+ .addRowRanges(
+ RowRange.newBuilder()
+ .setStartKeyClosed(ByteString.copyFromUtf8("simple-begin"))
+ .setEndKeyOpen(ByteString.copyFromUtf8("simple-end")))
+ .addRowRanges(
+ RowRange.newBuilder()
+ .setStartKeyClosed(ByteString.copyFromUtf8("byte-begin"))
+ .setEndKeyOpen(ByteString.copyFromUtf8("byte-end")))
+ .addRowRanges(
+ RowRange.newBuilder()
+ .setStartKeyClosed(ByteString.copyFromUtf8("range-begin"))
+ .setEndKeyOpen(ByteString.copyFromUtf8("range-end")));
+
+ ReadRowsRequest actualProto = query.toProto(requestContext);
+ assertThat(actualProto).isEqualTo(expectedProto.build());
+ }
+
+ @Test
+ public void filterTest() {
+ Query query = Query.create(TABLE_NAME.getTable()).filter(FILTERS.key().regex(".*"));
+
+ Builder expectedProto =
+ expectedProtoBuilder()
+ .setFilter(RowFilter.newBuilder().setRowKeyRegexFilter(ByteString.copyFromUtf8(".*")));
+
+ ReadRowsRequest actualProto = query.toProto(requestContext);
+ assertThat(actualProto).isEqualTo(expectedProto.build());
+ }
+
+ @Test
+ public void limitTest() {
+ Query query = Query.create(TABLE_NAME.getTable()).limit(10);
+
+ Builder expectedProto = expectedProtoBuilder().setRowsLimit(10);
+
+ ReadRowsRequest actualProto = query.toProto(requestContext);
+ assertThat(actualProto).isEqualTo(expectedProto.build());
+ }
+
+ private static ReadRowsRequest.Builder expectedProtoBuilder() {
+ return ReadRowsRequest.newBuilder()
+ .setTableName(TABLE_NAME.toString())
+ .setAppProfileId(APP_PROFILE_ID);
+ }
+}
diff --git a/google-cloud-bigtable/src/test/java/com/google/cloud/bigtable/data/v2/wrappers/RangeTest.java b/google-cloud-bigtable/src/test/java/com/google/cloud/bigtable/data/v2/wrappers/RangeTest.java
new file mode 100644
index 000000000000..e42542f32788
--- /dev/null
+++ b/google-cloud-bigtable/src/test/java/com/google/cloud/bigtable/data/v2/wrappers/RangeTest.java
@@ -0,0 +1,199 @@
+/*
+ * 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.common.truth.Truth.assertThat;
+
+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.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();
+ assertThat(range.getStartBound()).isEqualTo(BoundType.UNBOUNDED);
+ assertThat(range.getEndBound()).isEqualTo(BoundType.UNBOUNDED);
+
+ Throwable actualError = null;
+ try {
+ //noinspection ResultOfMethodCallIgnored
+ range.getStart();
+ } catch (Throwable e) {
+ actualError = e;
+ }
+ assertThat(actualError).isInstanceOf(IllegalStateException.class);
+
+ try {
+ //noinspection ResultOfMethodCallIgnored
+ range.getEnd();
+ } catch (Throwable e) {
+ actualError = e;
+ }
+ assertThat(actualError).isInstanceOf(IllegalStateException.class);
+ }
+
+ @Test
+ public void timestampOfTest() {
+ TimestampRange range = TimestampRange.create(10, 2_000);
+ assertThat(range.getStartBound()).isEqualTo(BoundType.CLOSED);
+ assertThat(range.getStart()).isEqualTo(10);
+ assertThat(range.getEndBound()).isEqualTo(BoundType.OPEN);
+ assertThat(range.getEnd()).isEqualTo(2_000);
+ }
+
+ @Test
+ public void timestampChangeStartTest() {
+ TimestampRange range = TimestampRange.create(10, 2_000).startOpen(20L);
+
+ assertThat(range.getEndBound()).isEqualTo(BoundType.OPEN);
+ assertThat(range.getEnd()).isEqualTo(2_000);
+
+ assertThat(range.getStartBound()).isEqualTo(BoundType.OPEN);
+ assertThat(range.getStart()).isEqualTo(20);
+
+ range = range.startClosed(30L);
+ assertThat(range.getStartBound()).isEqualTo(BoundType.CLOSED);
+ assertThat(range.getStart()).isEqualTo(30);
+ }
+
+ @Test
+ public void timestampChangeEndTest() {
+ TimestampRange range = TimestampRange.create(10, 2_000).endClosed(1_000L);
+
+ assertThat(range.getStartBound()).isEqualTo(BoundType.CLOSED);
+ assertThat(range.getStart()).isEqualTo(10);
+
+ assertThat(range.getEndBound()).isEqualTo(BoundType.CLOSED);
+ assertThat(range.getEnd()).isEqualTo(1_000);
+
+ range = range.endOpen(3_000L);
+ assertThat(range.getEndBound()).isEqualTo(BoundType.OPEN);
+ assertThat(range.getEnd()).isEqualTo(3_000);
+ }
+
+ @Test
+ public void byteStringUnboundedTest() {
+ ByteStringRange range = ByteStringRange.unbounded();
+ assertThat(range.getStartBound()).isEqualTo(BoundType.UNBOUNDED);
+ assertThat(range.getEndBound()).isEqualTo(BoundType.UNBOUNDED);
+
+ Throwable actualError = null;
+ try {
+ range.getStart();
+ } catch (Throwable e) {
+ actualError = e;
+ }
+ assertThat(actualError).isInstanceOf(IllegalStateException.class);
+
+ try {
+ range.getEnd();
+ } catch (Throwable e) {
+ actualError = e;
+ }
+ assertThat(actualError).isInstanceOf(IllegalStateException.class);
+ }
+
+ @Test
+ public void byteStringOfTest() {
+ ByteStringRange range =
+ ByteStringRange.create(ByteString.copyFromUtf8("a"), ByteString.copyFromUtf8("b"));
+
+ assertThat(range.getStartBound()).isEqualTo(BoundType.CLOSED);
+ assertThat(range.getStart()).isEqualTo(ByteString.copyFromUtf8("a"));
+ assertThat(range.getEndBound()).isEqualTo(BoundType.OPEN);
+ assertThat(range.getEnd()).isEqualTo(ByteString.copyFromUtf8("b"));
+ }
+
+ @Test
+ public void byteStringOfStringTest() {
+ ByteStringRange range = ByteStringRange.create("a", "b");
+
+ assertThat(range.getStartBound()).isEqualTo(BoundType.CLOSED);
+ assertThat(range.getStart()).isEqualTo(ByteString.copyFromUtf8("a"));
+ assertThat(range.getEndBound()).isEqualTo(BoundType.OPEN);
+ assertThat(range.getEnd()).isEqualTo(ByteString.copyFromUtf8("b"));
+ }
+
+ @Test
+ public void byteStringChangeStartTest() {
+ ByteStringRange range =
+ ByteStringRange.create(ByteString.copyFromUtf8("a"), ByteString.copyFromUtf8("z"))
+ .startOpen(ByteString.copyFromUtf8("b"));
+
+ assertThat(range.getEndBound()).isEqualTo(BoundType.OPEN);
+ assertThat(range.getEnd()).isEqualTo(ByteString.copyFromUtf8("z"));
+
+ assertThat(range.getStartBound()).isEqualTo(BoundType.OPEN);
+ assertThat(range.getStart()).isEqualTo(ByteString.copyFromUtf8("b"));
+
+ range = range.startClosed(ByteString.copyFromUtf8("c"));
+ assertThat(range.getStartBound()).isEqualTo(BoundType.CLOSED);
+ assertThat(range.getStart()).isEqualTo(ByteString.copyFromUtf8("c"));
+ }
+
+ @Test
+ public void byteStringChangeStartStringTest() {
+ ByteStringRange range = ByteStringRange.create("a", "z").startOpen("b");
+
+ assertThat(range.getEndBound()).isEqualTo(BoundType.OPEN);
+ assertThat(range.getEnd()).isEqualTo(ByteString.copyFromUtf8("z"));
+
+ assertThat(range.getStartBound()).isEqualTo(BoundType.OPEN);
+ assertThat(range.getStart()).isEqualTo(ByteString.copyFromUtf8("b"));
+
+ range = range.startClosed("c");
+ assertThat(range.getStartBound()).isEqualTo(BoundType.CLOSED);
+ assertThat(range.getStart()).isEqualTo(ByteString.copyFromUtf8("c"));
+ }
+
+ @Test
+ public void byteStringChangeEndTest() {
+ ByteStringRange range =
+ ByteStringRange.create(ByteString.copyFromUtf8("a"), ByteString.copyFromUtf8("z"))
+ .endClosed(ByteString.copyFromUtf8("y"));
+
+ assertThat(range.getStartBound()).isEqualTo(BoundType.CLOSED);
+ assertThat(range.getStart()).isEqualTo(ByteString.copyFromUtf8("a"));
+
+ assertThat(range.getEndBound()).isEqualTo(BoundType.CLOSED);
+ assertThat(range.getEnd()).isEqualTo(ByteString.copyFromUtf8("y"));
+
+ range = range.endOpen(ByteString.copyFromUtf8("x"));
+ assertThat(range.getEndBound()).isEqualTo(BoundType.OPEN);
+ assertThat(range.getEnd()).isEqualTo(ByteString.copyFromUtf8("x"));
+ }
+
+ @Test
+ public void byteStringChangeEndStringTest() {
+ ByteStringRange range = ByteStringRange.create("a", "z").endClosed("y");
+
+ assertThat(range.getStartBound()).isEqualTo(BoundType.CLOSED);
+ assertThat(range.getStart()).isEqualTo(ByteString.copyFromUtf8("a"));
+
+ assertThat(range.getEndBound()).isEqualTo(BoundType.CLOSED);
+ assertThat(range.getEnd()).isEqualTo(ByteString.copyFromUtf8("y"));
+
+ range = range.endOpen("x");
+ assertThat(range.getEndBound()).isEqualTo(BoundType.OPEN);
+ assertThat(range.getEnd()).isEqualTo(ByteString.copyFromUtf8("x"));
+ }
+}
diff --git a/google-cloud-bigtable/src/test/java/com/google/cloud/bigtable/data/v2/wrappers/RowTest.java b/google-cloud-bigtable/src/test/java/com/google/cloud/bigtable/data/v2/wrappers/RowTest.java
new file mode 100644
index 000000000000..ba0996fb0d47
--- /dev/null
+++ b/google-cloud-bigtable/src/test/java/com/google/cloud/bigtable/data/v2/wrappers/RowTest.java
@@ -0,0 +1,68 @@
+/*
+ * 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.common.truth.Truth.assertThat;
+
+import com.google.common.collect.ImmutableList;
+import com.google.protobuf.ByteString;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+@RunWith(JUnit4.class)
+public class RowTest {
+ @Test
+ public void compareTest() {
+ Row row1 =
+ Row.create(
+ ByteString.copyFromUtf8("key1"),
+ ImmutableList.of(
+ RowCell.create(
+ "family",
+ ByteString.EMPTY,
+ 1000,
+ ImmutableList.of(),
+ ByteString.copyFromUtf8("value"))));
+ Row row2 =
+ Row.create(
+ ByteString.copyFromUtf8("key2"),
+ ImmutableList.of(
+ RowCell.create(
+ "family",
+ ByteString.EMPTY,
+ 1000,
+ ImmutableList.of(),
+ ByteString.copyFromUtf8("value"))));
+ Row row2b =
+ Row.create(
+ ByteString.copyFromUtf8("key2"),
+ ImmutableList.of(
+ RowCell.create(
+ "family2",
+ ByteString.EMPTY,
+ 1000,
+ ImmutableList.of(),
+ ByteString.copyFromUtf8("value"))));
+
+ assertThat(row1).isEqualTo(row1);
+ assertThat(row1).isLessThan(row2);
+ assertThat(row2).isGreaterThan(row1);
+
+ // Comparator only cares about row keys
+ assertThat(row2).isEquivalentAccordingToCompareTo(row2b);
+ }
+}