diff --git a/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/BigtableDataClient.java b/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/BigtableDataClient.java index b003190a2271..39f2dd85ca2a 100644 --- a/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/BigtableDataClient.java +++ b/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/BigtableDataClient.java @@ -16,8 +16,14 @@ package com.google.cloud.bigtable.data.v2; import com.google.api.core.InternalApi; +import com.google.api.gax.rpc.ResponseObserver; +import com.google.api.gax.rpc.ServerStream; +import com.google.api.gax.rpc.ServerStreamingCallable; import com.google.bigtable.admin.v2.InstanceName; 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 com.google.cloud.bigtable.data.v2.wrappers.RowAdapter; import java.io.IOException; /** @@ -29,7 +35,9 @@ *
{@code
  * InstanceName instanceName = InstanceName.of("[PROJECT]", "[INSTANCE]");
  * try (BigtableDataClient bigtableDataClient = BigtableDataClient.create(instanceName)) {
- *   // TODO: add example usage
+ *   for(Row row : bigtableDataClient.readRows(Query.create("[TABLE]")) {
+ *     // Do something with row
+ *   }
  * }
  * }
* @@ -40,11 +48,13 @@ * methods: * *
    - *
  1. A "flattened" method. With this type of method, the fields of the request type have been - * converted into function parameters. It may be the case that not all fields are available as - * parameters, and not every API method will have a flattened method entry point. - *
  2. A "callable" method. This type of method takes no parameters and returns an immutable API - * callable object, which can be used to initiate calls to the service. + *
  3. A "flattened" method, like `readRows()`. With this type of method, the fields of the + * request type have been converted into function parameters. It may be the case that not all + * fields are available as parameters, and not every API method will have a flattened method + * entry point. + *
  4. A "callable" method, like `readRowsCallable()`. This type of method takes no parameters and + * returns an immutable API callable object, which can be used to initiate calls to the + * service. *
* *

See the individual methods for example code. @@ -107,6 +117,136 @@ public static BigtableDataClient create(BigtableDataSettings settings) throws IO this.stub = stub; } + /** + * Convenience method for synchronous streaming the results of a {@link Query}. + * + *

Sample code: + * + *

{@code
+   * // Import the filter DSL
+   * import static com.google.cloud.bigtable.data.v2.wrappers.Filters.FILTERS;
+   *
+   * InstanceName instanceName = InstanceName.of("[PROJECT]", "[INSTANCE]");
+   * try (BigtableClient bigtableClient = BigtableClient.create(instanceName)) {
+   *   String tableId = "[TABLE]";
+   *
+   *   Query query = Query.create(tableId)
+   *          .range("[START KEY]", "[END KEY]")
+   *          .filter(FILTERS.qualifier().regex("[COLUMN PREFIX].*"));
+   *
+   *   // Iterator style
+   *   for(Row row : bigtableClient.readRows(query)) {
+   *     // Do something with row
+   *   }
+   * }
+   * }
+ * + * @see ServerStreamingCallable For call styles. + * @see Query For query options. + * @see com.google.cloud.bigtable.data.v2.wrappers.Filters For the filter building DSL. + */ + public ServerStream readRows(Query query) { + return readRowsCallable().call(query); + } + + /** + * Convenience method for asynchronous streaming the results of a {@link Query}. + * + *

Sample code: + * + *

{@code
+   * InstanceName instanceName = InstanceName.of("[PROJECT]", "[INSTANCE]");
+   * try (BigtableClient bigtableClient = BigtableClient.create(instanceName)) {
+   *   String tableId = "[TABLE]";
+   *
+   *   Query query = Query.create(tableId)
+   *          .range("[START KEY]", "[END KEY]")
+   *          .filter(FILTERS.qualifier().regex("[COLUMN PREFIX].*"));
+   *
+   *   client.readRowsAsync(query, new ResponseObserver() {
+   *     public void onStart(StreamController controller) { }
+   *     public void onResponse(Row response) {
+   *       // Do something with Row
+   *     }
+   *     public void onError(Throwable t) {
+   *       // Handle error before the stream completes
+   *     }
+   *     public void onComplete() {
+   *       // Handle stream completion
+   *     }
+   *   });
+   * }
+   * }
+ */ + public void readRowsAsync(Query query, ResponseObserver observer) { + readRowsCallable().call(query, observer); + } + + /** + * Streams back the results of the query. The returned callable object allows for customization of + * api invocation. + * + *

Sample code: + * + *

{@code
+   * InstanceName instanceName = InstanceName.of("[PROJECT]", "[INSTANCE]");
+   * try (BigtableClient bigtableClient = BigtableClient.create(instanceName)) {
+   *   String tableId = "[TABLE]";
+   *
+   *   Query query = Query.create(tableId)
+   *          .range("[START KEY]", "[END KEY]")
+   *          .filter(FILTERS.qualifier().regex("[COLUMN PREFIX].*"));
+   *
+   *   // Iterator style
+   *   for(Row row : bigtableClient.readRowsCallable().call(query)) {
+   *     // Do something with row
+   *   }
+   *
+   *   // Point look up
+   *   ApiFuture rowFuture = bigtableClient.readRowsCallable().first().futureCall(query);
+   *
+   *   // etc
+   * }
+   * }
+ * + * @see ServerStreamingCallable For call styles. + * @see Query For query options. + * @see com.google.cloud.bigtable.data.v2.wrappers.Filters For the filter building DSL. + */ + public ServerStreamingCallable readRowsCallable() { + return stub.readRowsCallable(); + } + + /** + * Streams back the results of the query. This callable allows for customization of the logical + * representation of a row. It's meant for advanced use cases. + * + *

Sample code: + * + *

{@code
+   * InstanceName instanceName = InstanceName.of("[PROJECT]", "[INSTANCE]");
+   * try (BigtableClient bigtableClient = BigtableClient.create(instanceName)) {
+   *   String tableId = "[TABLE]";
+   *
+   *   Query query = Query.create(tableId)
+   *          .range("[START KEY]", "[END KEY]")
+   *          .filter(FILTERS.qualifier().regex("[COLUMN PREFIX].*"));
+   *
+   *   // Iterator style
+   *   for(CustomRow row : bigtableClient.readRowsCallable(new CustomRowAdapter()).call(query)) {
+   *     // Do something with row
+   *   }
+   * }
+   * }
+ * + * @see ServerStreamingCallable For call styles. + * @see Query For query options. + * @see com.google.cloud.bigtable.data.v2.wrappers.Filters For the filter building DSL. + */ + public ServerStreamingCallable readRowsCallable(RowAdapter rowAdapter) { + return stub.createReadRowsCallable(rowAdapter); + } + /** Close the clients and releases all associated resources. */ @Override public void close() throws Exception { diff --git a/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/BigtableDataSettings.java b/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/BigtableDataSettings.java index 9889b8061605..4926bb0e5221 100644 --- a/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/BigtableDataSettings.java +++ b/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/BigtableDataSettings.java @@ -16,8 +16,11 @@ package com.google.cloud.bigtable.data.v2; import com.google.api.gax.rpc.ClientSettings; +import com.google.api.gax.rpc.ServerStreamingCallSettings; import com.google.bigtable.admin.v2.InstanceName; import com.google.cloud.bigtable.data.v2.stub.EnhancedBigtableStubSettings; +import com.google.cloud.bigtable.data.v2.wrappers.Query; +import com.google.cloud.bigtable.data.v2.wrappers.Row; import java.io.IOException; import javax.annotation.Nonnull; @@ -38,10 +41,13 @@ * 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();
  * }
*/ public class BigtableDataSettings extends ClientSettings { @@ -64,6 +70,11 @@ public String getAppProfileId() { return getTypedStubSettings().getAppProfileId(); } + /** Returns the object with the settings used for calls to ReadRows. */ + public ServerStreamingCallSettings readRowsSettings() { + return getTypedStubSettings().readRowsSettings(); + } + @SuppressWarnings("unchecked") EnhancedBigtableStubSettings getTypedStubSettings() { return (EnhancedBigtableStubSettings) getStubSettings(); @@ -124,6 +135,11 @@ public String getAppProfileId() { return getTypedStubSettings().getAppProfileId(); } + /** Returns the builder for the settings used for calls to readRows. */ + public ServerStreamingCallSettings.Builder 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: + * + *
    + *
  1. Binary strings are supported. + *
  2. 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: + * + *

    + *
  1. Exactly 1 {@code startRow} for each row. + *
  2. Exactly 1 {@code startCell} for each cell. + *
  3. At least 1 {@code cellValue} for each cell. + *
  4. Exactly 1 {@code finishCell} for each cell. + *
  5. 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); + } +}