diff --git a/.cloudbuild/samples_build.yaml b/.cloudbuild/samples_build.yaml new file mode 100644 index 0000000000..06f87c837e --- /dev/null +++ b/.cloudbuild/samples_build.yaml @@ -0,0 +1,33 @@ +steps: +- name: gcr.io/cloud-devrel-public-resources/java8 + entrypoint: ls + args: [ + '-alt', + ] +- name: gcr.io/cloud-devrel-public-resources/java8 + entrypoint: curl + args: [ + '--header', + 'Metadata-Flavor: Google', + 'http://metadata.google.internal/computeMetadata/v1/instance/service-accounts/default/email' + ] +- name: gcr.io/cloud-devrel-public-resources/java8 + entrypoint: pwd +- name: gcr.io/cloud-devrel-public-resources/java8 + entrypoint: bash + args: [ + '.kokoro/build.sh' + ] + env: + - 'JOB_TYPE=samples' + - 'GOOGLE_CLOUD_PROJECT=cloud-java-ci-sample' + - 'BIGTABLE_TESTING_INSTANCE=instance' + - 'KOKORO_GITHUB_PULL_REQUEST_NUMBER=$_PR_NUMBER' +- name: gcr.io/cloud-devrel-public-resources/java8 + entrypoint: echo + args: [ + 'Sample job succeeded', + ] +timeout: 3600s +options: + defaultLogsBucketBehavior: REGIONAL_USER_OWNED_BUCKET diff --git a/.github/trusted-contribution.yml b/.github/trusted-contribution.yml index a0ba1f7d90..65c632ad45 100644 --- a/.github/trusted-contribution.yml +++ b/.github/trusted-contribution.yml @@ -1,3 +1,7 @@ trustedContributors: - renovate-bot - gcf-owl-bot[bot] + +annotations: +- type: comment + text: "/gcbrun" diff --git a/.github/workflows/unmanaged_dependency_check.yaml b/.github/workflows/unmanaged_dependency_check.yaml index 1a21cb9d8c..9e3961b938 100644 --- a/.github/workflows/unmanaged_dependency_check.yaml +++ b/.github/workflows/unmanaged_dependency_check.yaml @@ -14,6 +14,6 @@ jobs: shell: bash run: .kokoro/build.sh - name: Unmanaged dependency check - uses: googleapis/sdk-platform-java/java-shared-dependencies/unmanaged-dependency-check@google-cloud-shared-dependencies/v3.27.0 + uses: googleapis/sdk-platform-java/java-shared-dependencies/unmanaged-dependency-check@google-cloud-shared-dependencies/v3.28.1 with: bom-path: google-cloud-bigtable-bom/pom.xml diff --git a/.kokoro/presubmit/graalvm-native-17.cfg b/.kokoro/presubmit/graalvm-native-17.cfg index bd956e1c26..f97c4740aa 100644 --- a/.kokoro/presubmit/graalvm-native-17.cfg +++ b/.kokoro/presubmit/graalvm-native-17.cfg @@ -3,7 +3,7 @@ # Configure the docker image for kokoro-trampoline. env_vars: { key: "TRAMPOLINE_IMAGE" - value: "gcr.io/cloud-devrel-public-resources/graalvm_sdk_platform_b:3.27.0" + value: "gcr.io/cloud-devrel-public-resources/graalvm_sdk_platform_b:3.28.1" } env_vars: { diff --git a/.kokoro/presubmit/graalvm-native.cfg b/.kokoro/presubmit/graalvm-native.cfg index b4444225ac..2e7bab018f 100644 --- a/.kokoro/presubmit/graalvm-native.cfg +++ b/.kokoro/presubmit/graalvm-native.cfg @@ -3,7 +3,7 @@ # Configure the docker image for kokoro-trampoline. env_vars: { key: "TRAMPOLINE_IMAGE" - value: "gcr.io/cloud-devrel-public-resources/graalvm_sdk_platform_a:3.27.0" + value: "gcr.io/cloud-devrel-public-resources/graalvm_sdk_platform_a:3.28.1" } env_vars: { diff --git a/CHANGELOG.md b/CHANGELOG.md index 22123cb172..d8d587a3af 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,23 @@ # Changelog +## [2.37.0](https://github.com/googleapis/java-bigtable/compare/v2.36.0...v2.37.0) (2024-03-27) + + +### Features + +* Add admin APIs for AuthorizedView ([#2175](https://github.com/googleapis/java-bigtable/issues/2175)) ([13d1df3](https://github.com/googleapis/java-bigtable/commit/13d1df3910e3041b57485a09317f717932b21727)) +* Support AuthorizedView in bigtable data client ([#2177](https://github.com/googleapis/java-bigtable/issues/2177)) ([4b255d0](https://github.com/googleapis/java-bigtable/commit/4b255d0160b0ea30a4ab6453ccae278f40d32ee2)) + + +### Bug Fixes + +* Update the accounting of partial batch mutations ([#2149](https://github.com/googleapis/java-bigtable/issues/2149)) ([4158094](https://github.com/googleapis/java-bigtable/commit/4158094c34c85b1540197256f98e8880962fb13d)) + + +### Dependencies + +* Update shared dependencies ([#2174](https://github.com/googleapis/java-bigtable/issues/2174)) ([f313f14](https://github.com/googleapis/java-bigtable/commit/f313f1410f28df15438d83acaa74b715c752abdd)) + ## [2.36.0](https://github.com/googleapis/java-bigtable/compare/v2.35.1...v2.36.0) (2024-03-13) diff --git a/README.md b/README.md index 5b2e73b013..a57ff48bf5 100644 --- a/README.md +++ b/README.md @@ -42,7 +42,7 @@ If you are using Maven without the BOM, add this to your dependencies: com.google.cloud google-cloud-bigtable - 2.35.1 + 2.36.0 ``` @@ -57,13 +57,13 @@ implementation 'com.google.cloud:google-cloud-bigtable' If you are using Gradle without BOM, add this to your dependencies: ```Groovy -implementation 'com.google.cloud:google-cloud-bigtable:2.35.1' +implementation 'com.google.cloud:google-cloud-bigtable:2.36.0' ``` If you are using SBT, add this to your dependencies: ```Scala -libraryDependencies += "com.google.cloud" % "google-cloud-bigtable" % "2.35.1" +libraryDependencies += "com.google.cloud" % "google-cloud-bigtable" % "2.36.0" ``` @@ -609,7 +609,7 @@ Java is a registered trademark of Oracle and/or its affiliates. [kokoro-badge-link-5]: http://storage.googleapis.com/cloud-devrel-public/java/badges/java-bigtable/java11.html [stability-image]: https://img.shields.io/badge/stability-stable-green [maven-version-image]: https://img.shields.io/maven-central/v/com.google.cloud/google-cloud-bigtable.svg -[maven-version-link]: https://central.sonatype.com/artifact/com.google.cloud/google-cloud-bigtable/2.35.1 +[maven-version-link]: https://central.sonatype.com/artifact/com.google.cloud/google-cloud-bigtable/2.36.0 [authentication]: https://github.com/googleapis/google-cloud-java#authentication [auth-scopes]: https://developers.google.com/identity/protocols/oauth2/scopes [predefined-iam-roles]: https://cloud.google.com/iam/docs/understanding-roles#predefined_roles diff --git a/google-cloud-bigtable-bom/pom.xml b/google-cloud-bigtable-bom/pom.xml index c9d7053577..9b0a9e5a4f 100644 --- a/google-cloud-bigtable-bom/pom.xml +++ b/google-cloud-bigtable-bom/pom.xml @@ -3,12 +3,12 @@ 4.0.0 com.google.cloud google-cloud-bigtable-bom - 2.36.0 + 2.37.0 pom com.google.cloud sdk-platform-java-config - 3.27.0 + 3.28.1 @@ -63,42 +63,42 @@ com.google.cloud google-cloud-bigtable - 2.36.0 + 2.37.0 com.google.cloud google-cloud-bigtable-emulator - 0.173.0 + 0.174.0 com.google.cloud google-cloud-bigtable-emulator-core - 0.173.0 + 0.174.0 com.google.api.grpc grpc-google-cloud-bigtable-admin-v2 - 2.36.0 + 2.37.0 com.google.api.grpc grpc-google-cloud-bigtable-v2 - 2.36.0 + 2.37.0 com.google.api.grpc proto-google-cloud-bigtable-admin-v2 - 2.36.0 + 2.37.0 com.google.api.grpc proto-google-cloud-bigtable-v2 - 2.36.0 + 2.37.0 com.google.cloud google-cloud-bigtable-stats - 2.36.0 + 2.37.0 diff --git a/google-cloud-bigtable-deps-bom/pom.xml b/google-cloud-bigtable-deps-bom/pom.xml index b3124fa415..d6db550f52 100644 --- a/google-cloud-bigtable-deps-bom/pom.xml +++ b/google-cloud-bigtable-deps-bom/pom.xml @@ -7,13 +7,13 @@ com.google.cloud sdk-platform-java-config - 3.27.0 + 3.28.1 com.google.cloud google-cloud-bigtable-deps-bom - 2.36.0 + 2.37.0 pom @@ -66,7 +66,7 @@ com.google.cloud gapic-libraries-bom - 1.31.0 + 1.32.0 pom import diff --git a/google-cloud-bigtable-emulator-core/pom.xml b/google-cloud-bigtable-emulator-core/pom.xml index a6cef1a89a..f8d73ba00a 100644 --- a/google-cloud-bigtable-emulator-core/pom.xml +++ b/google-cloud-bigtable-emulator-core/pom.xml @@ -7,11 +7,11 @@ google-cloud-bigtable-parent com.google.cloud - 2.36.0 + 2.37.0 google-cloud-bigtable-emulator-core - 0.173.0 + 0.174.0 A Java wrapper for the Cloud Bigtable emulator. diff --git a/google-cloud-bigtable-emulator/pom.xml b/google-cloud-bigtable-emulator/pom.xml index aa9688af77..fd783e146b 100644 --- a/google-cloud-bigtable-emulator/pom.xml +++ b/google-cloud-bigtable-emulator/pom.xml @@ -5,7 +5,7 @@ 4.0.0 google-cloud-bigtable-emulator - 0.173.0 + 0.174.0 Google Cloud Java - Bigtable Emulator https://github.com/googleapis/java-bigtable @@ -14,7 +14,7 @@ com.google.cloud google-cloud-bigtable-parent - 2.36.0 + 2.37.0 scm:git:git@github.com:googleapis/java-bigtable.git @@ -81,14 +81,14 @@ com.google.cloud google-cloud-bigtable-deps-bom - 2.36.0 + 2.37.0 pom import com.google.cloud google-cloud-bigtable-bom - 2.36.0 + 2.37.0 pom import @@ -99,7 +99,7 @@ com.google.cloud google-cloud-bigtable-emulator-core - 0.173.0 + 0.174.0 diff --git a/google-cloud-bigtable-stats/pom.xml b/google-cloud-bigtable-stats/pom.xml index caa2cfbaec..638d0ce2a7 100644 --- a/google-cloud-bigtable-stats/pom.xml +++ b/google-cloud-bigtable-stats/pom.xml @@ -5,7 +5,7 @@ com.google.cloud google-cloud-bigtable-parent - 2.36.0 + 2.37.0 4.0.0 @@ -13,7 +13,7 @@ through Stackdriver. Built-in metrics will be implemented with shaded OpenCensus so it won't interfere with customer's application metrics. --> google-cloud-bigtable-stats - 2.36.0 + 2.37.0 Experimental project to shade OpenCensus dependencies. @@ -21,7 +21,7 @@ com.google.cloud google-cloud-bigtable-deps-bom - 2.36.0 + 2.37.0 pom import diff --git a/google-cloud-bigtable/clirr-ignored-differences.xml b/google-cloud-bigtable/clirr-ignored-differences.xml index 756567f300..7ac7946561 100644 --- a/google-cloud-bigtable/clirr-ignored-differences.xml +++ b/google-cloud-bigtable/clirr-ignored-differences.xml @@ -140,6 +140,13 @@ * * + + + 7005 + com/google/cloud/bigtable/data/v2/stub/mutaterows/MutateRowsBatchingDescriptor + * + * + 7002 diff --git a/google-cloud-bigtable/pom.xml b/google-cloud-bigtable/pom.xml index 038391236c..43619f3cf9 100644 --- a/google-cloud-bigtable/pom.xml +++ b/google-cloud-bigtable/pom.xml @@ -2,7 +2,7 @@ 4.0.0 google-cloud-bigtable - 2.36.0 + 2.37.0 jar Google Cloud Bigtable https://github.com/googleapis/java-bigtable @@ -12,11 +12,11 @@ com.google.cloud google-cloud-bigtable-parent - 2.36.0 + 2.37.0 - 2.36.0 + 2.37.0 google-cloud-bigtable @@ -47,14 +47,14 @@ com.google.cloud google-cloud-bigtable-deps-bom - 2.36.0 + 2.37.0 pom import com.google.cloud google-cloud-bigtable-bom - 2.36.0 + 2.37.0 pom import diff --git a/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/Version.java b/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/Version.java index 937638f162..e9befe0974 100644 --- a/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/Version.java +++ b/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/Version.java @@ -20,6 +20,6 @@ @InternalApi("For internal use only") public final class Version { // {x-version-update-start:google-cloud-bigtable:current} - public static String VERSION = "2.36.0"; + public static String VERSION = "2.37.0"; // {x-version-update-end} } diff --git a/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/admin/v2/BigtableTableAdminClient.java b/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/admin/v2/BigtableTableAdminClient.java index f83b01b3d4..f640bb6a30 100644 --- a/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/admin/v2/BigtableTableAdminClient.java +++ b/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/admin/v2/BigtableTableAdminClient.java @@ -22,11 +22,14 @@ import com.google.api.gax.longrunning.OperationFuture; import com.google.api.gax.rpc.ApiExceptions; import com.google.api.gax.rpc.NotFoundException; +import com.google.bigtable.admin.v2.DeleteAuthorizedViewRequest; import com.google.bigtable.admin.v2.DeleteBackupRequest; import com.google.bigtable.admin.v2.DeleteTableRequest; import com.google.bigtable.admin.v2.DropRowRangeRequest; +import com.google.bigtable.admin.v2.GetAuthorizedViewRequest; import com.google.bigtable.admin.v2.GetBackupRequest; import com.google.bigtable.admin.v2.GetTableRequest; +import com.google.bigtable.admin.v2.ListAuthorizedViewsRequest; import com.google.bigtable.admin.v2.ListBackupsRequest; import com.google.bigtable.admin.v2.ListTablesRequest; import com.google.bigtable.admin.v2.RestoreTableMetadata; @@ -34,13 +37,17 @@ import com.google.bigtable.admin.v2.Table.View; import com.google.cloud.Policy; import com.google.cloud.Policy.DefaultMarshaller; +import com.google.cloud.bigtable.admin.v2.BaseBigtableTableAdminClient.ListAuthorizedViewsPage; +import com.google.cloud.bigtable.admin.v2.BaseBigtableTableAdminClient.ListAuthorizedViewsPagedResponse; import com.google.cloud.bigtable.admin.v2.BaseBigtableTableAdminClient.ListBackupsPage; import com.google.cloud.bigtable.admin.v2.BaseBigtableTableAdminClient.ListBackupsPagedResponse; import com.google.cloud.bigtable.admin.v2.BaseBigtableTableAdminClient.ListTablesPage; import com.google.cloud.bigtable.admin.v2.BaseBigtableTableAdminClient.ListTablesPagedResponse; import com.google.cloud.bigtable.admin.v2.internal.NameUtil; +import com.google.cloud.bigtable.admin.v2.models.AuthorizedView; import com.google.cloud.bigtable.admin.v2.models.Backup; import com.google.cloud.bigtable.admin.v2.models.CopyBackupRequest; +import com.google.cloud.bigtable.admin.v2.models.CreateAuthorizedViewRequest; import com.google.cloud.bigtable.admin.v2.models.CreateBackupRequest; import com.google.cloud.bigtable.admin.v2.models.CreateTableRequest; import com.google.cloud.bigtable.admin.v2.models.EncryptionInfo; @@ -50,6 +57,7 @@ import com.google.cloud.bigtable.admin.v2.models.RestoreTableRequest; import com.google.cloud.bigtable.admin.v2.models.RestoredTableResult; import com.google.cloud.bigtable.admin.v2.models.Table; +import com.google.cloud.bigtable.admin.v2.models.UpdateAuthorizedViewRequest; import com.google.cloud.bigtable.admin.v2.models.UpdateBackupRequest; import com.google.cloud.bigtable.admin.v2.models.UpdateTableRequest; import com.google.cloud.bigtable.admin.v2.stub.EnhancedBigtableTableAdminStub; @@ -1434,6 +1442,352 @@ public ApiFuture awaitReplicationAsync(final String tableId) { return stub.awaitReplicationCallable().futureCall(tableName); } + /** + * Creates a new authorized view with the specified configuration. + * + *

Sample code: + * + *

{@code
+   * CreateAuthorizedViewRequest request =
+   *     CreateAuthorizedViewRequest.of("my-table", "my-new-authorized-view")
+   *         .setDeletionProtection(true)
+   *         .setAuthorizedViewType(
+   *             SubsetView.create()
+   *                 .addRowPrefix("row#")
+   *                 .addFamilySubsets(
+   *                     "my-family", FamilySubsets.create().addQualifier("column")));
+   *
+   * AuthorizedView response = client.createAuthorizedView(request);
+   * }
+ * + * @see CreateAuthorizedViewRequest for available options. + */ + public AuthorizedView createAuthorizedView(CreateAuthorizedViewRequest request) { + return ApiExceptions.callAndTranslateApiException(createAuthorizedViewAsync(request)); + } + + /** + * Asynchronously creates a new authorized view with the specified configuration. + * + *

Sample code: + * + *

{@code
+   * CreateAuthorizedViewRequest request =
+   *     CreateAuthorizedViewRequest.of("my-table", "my-new-authorized-view")
+   *         .setDeletionProtection(true)
+   *         .setAuthorizedViewType(
+   *             SubsetView.create()
+   *                 .addRowPrefix("row#")
+   *                 .addFamilySubsets(
+   *                     "my-family", FamilySubsets.create().addQualifier("column")));
+   *
+   * ApiFuture future = client.createAuthorizedViewAsync(request);
+   *
+   * ApiFutures.addCallback(
+   *   future,
+   *   new ApiFutureCallback() {
+   *     public void onSuccess(AuthorizedView authorizedView) {
+   *       System.out.println("Successfully created the authorized view: " + authorizedView.getId());
+   *     }
+   *
+   *     public void onFailure(Throwable t) {
+   *       t.printStackTrace();
+   *     }
+   *   },
+   *   MoreExecutors.directExecutor()
+   * );
+   * }
+ * + * @see CreateAuthorizedViewRequest for available options. + */ + public ApiFuture createAuthorizedViewAsync(CreateAuthorizedViewRequest request) { + return ApiFutures.transform( + stub.createAuthorizedViewOperationCallable() + .futureCall(request.toProto(projectId, instanceId)), + new ApiFunction() { + @Override + public AuthorizedView apply( + com.google.bigtable.admin.v2.AuthorizedView authorizedViewProto) { + return AuthorizedView.fromProto(authorizedViewProto); + } + }, + MoreExecutors.directExecutor()); + } + + /** + * Updates an existing authorized view with the specified configuration. + * + *

Sample code: + * + *

{@code
+   * AuthorizedView existingAuthorizedView = client.getAuthorizedView("my-table", "my-authorized-view");
+   *
+   * UpdateAuthorizedViewRequest request =
+   *     UpdateAuthorizedViewRequest.of(existingAuthorizedView).setDeletionProtection(true);
+   *
+   * AuthorizedView response = client.updateAuthorizedView(request);
+   * }
+ * + * @see UpdateAuthorizedViewRequest for available options. + */ + public AuthorizedView updateAuthorizedView(UpdateAuthorizedViewRequest request) { + return ApiExceptions.callAndTranslateApiException(updateAuthorizedViewAsync(request)); + } + + /** + * Asynchronously updates an existing authorized view with the specified configuration. + * + *

Sample code: + * + *

{@code
+   * AuthorizedView existingAuthorizedView = client.getAuthorizedView("my-table", "my-authorized-view");
+   *
+   * UpdateAuthorizedViewRequest request =
+   *     UpdateAuthorizedViewRequest.of(existingAuthorizedView).setDeletionProtection(true);
+   *
+   * ApiFuture future = client.updateAuthorizedViewAsync(request);
+   *
+   * ApiFutures.addCallback(
+   *   future,
+   *   new ApiFutureCallback() {
+   *     public void onSuccess(AuthorizedView authorizedView) {
+   *       System.out.println("Successfully updated the authorized view: " + authorizedView.getId());
+   *     }
+   *
+   *     public void onFailure(Throwable t) {
+   *       t.printStackTrace();
+   *     }
+   *   },
+   *   MoreExecutors.directExecutor()
+   * );
+   * }
+ * + * @see UpdateAuthorizedViewRequest for available options. + */ + public ApiFuture updateAuthorizedViewAsync(UpdateAuthorizedViewRequest request) { + return ApiFutures.transform( + stub.updateAuthorizedViewOperationCallable() + .futureCall(request.toProto(projectId, instanceId)), + new ApiFunction() { + @Override + public AuthorizedView apply( + com.google.bigtable.admin.v2.AuthorizedView authorizedViewProto) { + return AuthorizedView.fromProto(authorizedViewProto); + } + }, + MoreExecutors.directExecutor()); + } + + /** + * Gets an authorized view with the specified authorized view ID in the specified table. + * + *

Sample code: + * + *

{@code
+   * AuthorizedView authorizedView = client.getAuthorizedView("my-table", "my-authorized-view");
+   * }
+ */ + public AuthorizedView getAuthorizedView(String tableId, String authorizedViewId) { + return ApiExceptions.callAndTranslateApiException( + getAuthorizedViewAsync(tableId, authorizedViewId)); + } + + /** + * Asynchronously gets an authorized view with the specified authorized view ID in the specified + * table. + * + *

Sample code: + * + *

{@code
+   * ApiFuture future = client.getAuthorizedViewAsync("my-table", "my-authorized-view");
+   *
+   * ApiFutures.addCallback(
+   *   future,
+   *   new ApiFutureCallback() {
+   *     public void onSuccess(AuthorizedView authorizedView) {
+   *       System.out.println("Successfully get the authorized view: " + authorizedView.getId());
+   *     }
+   *
+   *     public void onFailure(Throwable t) {
+   *       t.printStackTrace();
+   *     }
+   *   },
+   *   MoreExecutors.directExecutor()
+   * );
+   * }
+ */ + public ApiFuture getAuthorizedViewAsync(String tableId, String authorizedViewId) { + GetAuthorizedViewRequest request = + GetAuthorizedViewRequest.newBuilder() + .setName( + NameUtil.formatAuthorizedViewName(projectId, instanceId, tableId, authorizedViewId)) + .build(); + return ApiFutures.transform( + stub.getAuthorizedViewCallable().futureCall(request), + new ApiFunction() { + @Override + public AuthorizedView apply( + com.google.bigtable.admin.v2.AuthorizedView authorizedViewProto) { + return AuthorizedView.fromProto(authorizedViewProto); + } + }, + MoreExecutors.directExecutor()); + } + + /** + * Lists all authorized view IDs in the specified table. + * + *

Sample code: + * + *

{@code
+   * List authorizedViews = client.listAuthorizedViews("my-table");
+   * }
+ */ + public List listAuthorizedViews(String tableId) { + return ApiExceptions.callAndTranslateApiException(listAuthorizedViewsAsync(tableId)); + } + + /** + * Asynchronously lists all authorized view IDs in the specified table. + * + *

Sample code: + * + *

{@code
+   * ApiFuture> future = client.listAuthorizedViewsAsync("my-table");
+   *
+   * ApiFutures.addCallback(
+   *   future,
+   *   new ApiFutureCallback>() {
+   *     public void onSuccess(List authorizedViewIds) {
+   *       System.out.println("Successfully get list of authorized views:");
+   *       for (AuthorizedView authorizedViewId : authorizedViewIds) {
+   *         System.out.println(authorizedViewId);
+   *       }
+   *     }
+   *
+   *     public void onFailure(Throwable t) {
+   *       t.printStackTrace();
+   *     }
+   *   },
+   *   MoreExecutors.directExecutor()
+   * );
+   * }
+ */ + public ApiFuture> listAuthorizedViewsAsync(String tableId) { + ListAuthorizedViewsRequest request = + ListAuthorizedViewsRequest.newBuilder() + .setParent(NameUtil.formatTableName(projectId, instanceId, tableId)) + .build(); + + // TODO(igorbernstein2): try to upstream pagination spooling or figure out a way to expose the + // paginated responses while maintaining the wrapper facade. + + // Fetches the first page. + ApiFuture firstPageFuture = + ApiFutures.transform( + stub.listAuthorizedViewsPagedCallable().futureCall(request), + new ApiFunction() { + @Override + public ListAuthorizedViewsPage apply(ListAuthorizedViewsPagedResponse response) { + return response.getPage(); + } + }, + MoreExecutors.directExecutor()); + + // Fetches the rest of the pages by chaining the futures. + ApiFuture> allProtos = + ApiFutures.transformAsync( + firstPageFuture, + new ApiAsyncFunction< + ListAuthorizedViewsPage, List>() { + List responseAccumulator = + Lists.newArrayList(); + + @Override + public ApiFuture> apply( + ListAuthorizedViewsPage page) { + // Add all entries from the page + responseAccumulator.addAll(Lists.newArrayList(page.getValues())); + + // If this is the last page, just return the accumulated responses. + if (!page.hasNextPage()) { + return ApiFutures.immediateFuture(responseAccumulator); + } + + // Otherwise fetch the next page. + return ApiFutures.transformAsync( + page.getNextPageAsync(), this, MoreExecutors.directExecutor()); + } + }, + MoreExecutors.directExecutor()); + + // Wraps all of the accumulated protos. + return ApiFutures.transform( + allProtos, + new ApiFunction, List>() { + @Override + public List apply(List protos) { + List results = Lists.newArrayListWithCapacity(protos.size()); + for (com.google.bigtable.admin.v2.AuthorizedView proto : protos) { + results.add(NameUtil.extractAuthorizedViewIdFromAuthorizedViewName(proto.getName())); + } + return results; + } + }, + MoreExecutors.directExecutor()); + } + + /** + * Deletes an authorized view with the specified authorized view ID in the specified table. Note + * that the deletion is prohibited if the authorized view has deletion_protection field set to + * true. + * + *

Sample code: + * + *

{@code
+   * client.deleteAuthorizedView("my-table", "my-authorized-view");
+   * }
+ */ + public void deleteAuthorizedView(String tableId, String authorizedViewId) { + ApiExceptions.callAndTranslateApiException( + deleteAuthorizedViewAsync(tableId, authorizedViewId)); + } + + /** + * Asynchronously deletes an authorized view with the specified authorized view ID in the + * specified table. Note that the deletion is prohibited if the authorized view has + * deletion_protection field set to true. + * + *

Sample code: + * + *

{@code
+   * ApiFuture future = client.deleteAuthorizedViewAsync("my-table", "my-authorized-view");
+   *
+   * ApiFutures.addCallback(
+   *   future,
+   *   new ApiFutureCallback() {
+   *     public void onSuccess(Void ignored) {
+   *       System.out.println("Successfully deleted the authorized view");
+   *     }
+   *
+   *     public void onFailure(Throwable t) {
+   *       t.printStackTrace();
+   *     }
+   *   },
+   *   MoreExecutors.directExecutor()
+   * );
+   * }
+ */ + public ApiFuture deleteAuthorizedViewAsync(String tableId, String authorizedViewId) { + DeleteAuthorizedViewRequest request = + DeleteAuthorizedViewRequest.newBuilder() + .setName( + NameUtil.formatAuthorizedViewName(projectId, instanceId, tableId, authorizedViewId)) + .build(); + + return transformToVoid(this.stub.deleteAuthorizedViewCallable().futureCall(request)); + } + /** * Helper method to construct the table name in format: * projects/{project}/instances/{instance}/tables/{tableId} @@ -1818,6 +2172,187 @@ public ApiFuture> testBackupIamPermissionAsync( return testResourceIamPermissions(backupName, permissions); } + /** + * Gets the IAM access control policy for the specified authorized view. + * + *

Sample code: + * + *

{@code
+   * Policy policy = client.getAuthorizedViewIamPolicy("my-table-id", "my-authorized-view-id");
+   * for(Map.Entry> entry : policy.getBindings().entrySet()) {
+   *   System.out.printf("Role: %s Identities: %s\n", entry.getKey(), entry.getValue());
+   * }
+   * }
+ * + * @see Table-level + * IAM management + */ + @SuppressWarnings("WeakerAccess") + public Policy getAuthorizedViewIamPolicy(String tableId, String authorizedViewId) { + return ApiExceptions.callAndTranslateApiException( + getAuthorizedViewIamPolicyAsync(tableId, authorizedViewId)); + } + + /** + * Asynchronously gets the IAM access control policy for the specified authorized view. + * + *

Sample code: + * + *

{@code
+   * ApiFuture policyFuture = client.getAuthorizedViewIamPolicyAsync("my-table-id", "my-authorized-view-id");
+   *
+   * ApiFutures.addCallback(policyFuture,
+   *   new ApiFutureCallback() {
+   *     public void onSuccess(Policy policy) {
+   *       for (Entry> entry : policy.getBindings().entrySet()) {
+   *         System.out.printf("Role: %s Identities: %s\n", entry.getKey(), entry.getValue());
+   *       }
+   *     }
+   *
+   *     public void onFailure(Throwable t) {
+   *       t.printStackTrace();
+   *     }
+   *   },
+   *   MoreExecutors.directExecutor());
+   * }
+ * + * @see Table-level + * IAM management + */ + @SuppressWarnings("WeakerAccess") + public ApiFuture getAuthorizedViewIamPolicyAsync( + String tableId, String authorizedViewId) { + String authorizedViewName = + NameUtil.formatAuthorizedViewName(projectId, instanceId, tableId, authorizedViewId); + return getResourceIamPolicy(authorizedViewName); + } + + /** + * Replaces the IAM policy associated with the specified authorized view. + * + *

Sample code: + * + *

{@code
+   * Policy newPolicy = client.setAuthorizedViewIamPolicy("my-table-id", "my-authorized-view-id",
+   *   Policy.newBuilder()
+   *     .addIdentity(Role.of("bigtable.user"), Identity.user("someone@example.com"))
+   *     .addIdentity(Role.of("bigtable.admin"), Identity.group("admins@example.com"))
+   *     .build());
+   * }
+ * + * @see Table-level + * IAM management + */ + @SuppressWarnings("WeakerAccess") + public Policy setAuthorizedViewIamPolicy(String tableId, String authorizedViewId, Policy policy) { + return ApiExceptions.callAndTranslateApiException( + setAuthorizedViewIamPolicyAsync(tableId, authorizedViewId, policy)); + } + + /** + * Asynchronously replaces the IAM policy associated with the specified authorized view. + * + *

Sample code: + * + *

{@code
+   * ApiFuture newPolicyFuture = client.setAuthorizedViewIamPolicyAsync("my-table-id", "my-authorized-view-id",
+   *   Policy.newBuilder()
+   *     .addIdentity(Role.of("bigtable.user"), Identity.user("someone@example.com"))
+   *     .addIdentity(Role.of("bigtable.admin"), Identity.group("admins@example.com"))
+   *     .build());
+   *
+   * ApiFutures.addCallback(newPolicyFuture,
+   *   new ApiFutureCallback() {
+   *     public void onSuccess(Policy policy) {
+   *       for (Entry> entry : policy.getBindings().entrySet()) {
+   *         System.out.printf("Role: %s Identities: %s\n", entry.getKey(), entry.getValue());
+   *       }
+   *     }
+   *
+   *     public void onFailure(Throwable t) {
+   *       t.printStackTrace();
+   *     }
+   *   },
+   *   MoreExecutors.directExecutor());
+   * }
+ * + * @see Table-level + * IAM management + */ + @SuppressWarnings("WeakerAccess") + public ApiFuture setAuthorizedViewIamPolicyAsync( + String tableId, String authorizedViewId, Policy policy) { + String authorizedViewName = + NameUtil.formatAuthorizedViewName(projectId, instanceId, tableId, authorizedViewId); + return setResourceIamPolicy(policy, authorizedViewName); + } + + /** + * Tests whether the caller has the given permissions for the specified authorized view. Returns a + * subset of the specified permissions that the caller has. + * + *

Sample code: + * + *

{@code
+   * List grantedPermissions = client.testAuthorizedViewIamPermission("my-table-id", "my-authorized-view-id",
+   *   "bigtable.authorizedViews.get", "bigtable.authorizedViews.delete");
+   * }
+ * + * System.out.println("Has get access: " + + * grantedPermissions.contains("bigtable.authorizedViews.get")); + * + *

System.out.println("Has delete access: " + + * grantedPermissions.contains("bigtable.authorizedViews.delete")); + * + * @see Cloud Bigtable + * permissions + */ + @SuppressWarnings({"WeakerAccess"}) + public List testAuthorizedViewIamPermission( + String tableId, String authorizedViewId, String... permissions) { + return ApiExceptions.callAndTranslateApiException( + testAuthorizedViewIamPermissionAsync(tableId, authorizedViewId, permissions)); + } + + /** + * Asynchronously tests whether the caller has the given permissions for the specified authorized + * view. Returns a subset of the specified permissions that the caller has. + * + *

Sample code: + * + *

{@code
+   * ApiFuture> grantedPermissionsFuture = client.testAuthorizedViewIamPermissionAsync("my-table-id", "my-authorized-view-id",
+   *   "bigtable.authorizedViews.get", "bigtable.authorizedViews.delete");
+   *
+   * ApiFutures.addCallback(grantedPermissionsFuture,
+   *   new ApiFutureCallback>() {
+   *     public void onSuccess(List grantedPermissions) {
+   *       System.out.println("Has get access: " + grantedPermissions.contains("bigtable.authorizedViews.get"));
+   *       System.out.println("Has delete access: " + grantedPermissions.contains("bigtable.authorizedViews.delete"));
+   *     }
+   *
+   *     public void onFailure(Throwable t) {
+   *       t.printStackTrace();
+   *     }
+   *   },
+   *   MoreExecutors.directExecutor());
+   * }
+ * + * @see Cloud Bigtable + * permissions + */ + @SuppressWarnings({"WeakerAccess"}) + public ApiFuture> testAuthorizedViewIamPermissionAsync( + String tableId, String authorizedViewId, String... permissions) { + String authorizedViewName = + NameUtil.formatAuthorizedViewName(projectId, instanceId, tableId, authorizedViewId); + return testResourceIamPermissions(authorizedViewName, permissions); + } + private ApiFuture getResourceIamPolicy(String name) { GetIamPolicyRequest request = GetIamPolicyRequest.newBuilder().setResource(name).build(); diff --git a/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/admin/v2/internal/NameUtil.java b/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/admin/v2/internal/NameUtil.java index 8cccf3d578..a2b59d6b5b 100644 --- a/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/admin/v2/internal/NameUtil.java +++ b/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/admin/v2/internal/NameUtil.java @@ -34,6 +34,9 @@ public class NameUtil { private static final Pattern BACKUP_PATTERN = Pattern.compile("projects/([^/]+)/instances/([^/]+)/clusters/([^/]+)/backups/([^/]+)"); + private static final Pattern AUTHORIZED_VIEW_PATTERN = + Pattern.compile("projects/([^/]+)/instances/([^/]+)/tables/([^/]+)/authorizedViews/([^/]+)"); + public static String formatProjectName(String projectId) { return "projects/" + projectId; } @@ -55,6 +58,11 @@ public static String formatBackupName( return formatClusterName(projectId, instanceId, clusterId) + "/backups/" + backupId; } + public static String formatAuthorizedViewName( + String projectId, String instanceId, String tableId, String viewId) { + return formatTableName(projectId, instanceId, tableId) + "/authorizedViews/" + viewId; + } + public static String extractTableIdFromTableName(String fullTableName) { Matcher matcher = TABLE_PATTERN.matcher(fullTableName); if (!matcher.matches()) { @@ -71,6 +79,15 @@ public static String extractBackupIdFromBackupName(String fullBackupName) { return matcher.group(4); } + public static String extractAuthorizedViewIdFromAuthorizedViewName( + String fullAuthorizedViewName) { + Matcher matcher = AUTHORIZED_VIEW_PATTERN.matcher(fullAuthorizedViewName); + if (!matcher.matches()) { + throw new IllegalArgumentException("Invalid authorized view name: " + fullAuthorizedViewName); + } + return matcher.group(4); + } + public static String extractZoneIdFromLocationName(String fullLocationName) { Matcher matcher = LOCATION_PATTERN.matcher(fullLocationName); if (!matcher.matches()) { diff --git a/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/admin/v2/models/AuthorizedView.java b/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/admin/v2/models/AuthorizedView.java new file mode 100644 index 0000000000..33e40f6458 --- /dev/null +++ b/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/admin/v2/models/AuthorizedView.java @@ -0,0 +1,124 @@ +/* + * Copyright 2024 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.admin.v2.models; + +import com.google.api.core.InternalApi; +import com.google.api.core.InternalExtensionOnly; +import com.google.bigtable.admin.v2.AuthorizedViewName; +import com.google.common.base.Objects; +import com.google.common.base.Preconditions; +import javax.annotation.Nonnull; + +/** + * A class that wraps the {@link com.google.bigtable.admin.v2.AuthorizedView} protocol buffer + * object. + * + *

An AuthorizedView represents subsets of a particular table based on rules. The access to each + * AuthorizedView can be configured separately from the Table. + * + *

Users can perform read/write operation on an AuthorizedView by providing an authorizedView id + * besides a table id, in which case the semantics remain identical as reading/writing on a Table + * except that visibility is restricted to the subset of the Table that the AuthorizedView + * represents. + */ +public final class AuthorizedView { + private final com.google.bigtable.admin.v2.AuthorizedView proto; + + /** + * Wraps the protobuf. This method is considered an internal implementation detail and not meant + * to be used by applications. + */ + @InternalApi + public static AuthorizedView fromProto( + @Nonnull com.google.bigtable.admin.v2.AuthorizedView proto) { + return new AuthorizedView(proto); + } + + private AuthorizedView(@Nonnull com.google.bigtable.admin.v2.AuthorizedView proto) { + Preconditions.checkNotNull(proto); + Preconditions.checkArgument(!proto.getName().isEmpty(), "AuthorizedView must have a name"); + Preconditions.checkArgument( + proto.hasSubsetView(), "AuthorizedView must have a subset_view field"); + this.proto = proto; + } + + /** Gets the authorized view's id. */ + public String getId() { + // Constructor ensures that name is not null. + AuthorizedViewName fullName = AuthorizedViewName.parse(proto.getName()); + + //noinspection ConstantConditions + return fullName.getAuthorizedView(); + } + + /** Gets the id of the table that owns this authorized view. */ + public String getTableId() { + // Constructor ensures that name is not null. + AuthorizedViewName fullName = AuthorizedViewName.parse(proto.getName()); + + //noinspection ConstantConditions + return fullName.getTable(); + } + + /** Returns whether this authorized view is deletion protected. */ + public boolean isDeletionProtected() { + return proto.getDeletionProtection(); + } + + /** Gets the type of this authorized view, which currently can only be a subset view. */ + public AuthorizedViewType getAuthorizedViewType() { + if (proto.hasSubsetView()) { + return SubsetView.fromProto(proto.getSubsetView()); + } else { + // Should never happen because the constructor verifies that one must exist. + throw new IllegalStateException("This AuthorizedView doesn't have a valid type specified"); + } + } + + /** + * Creates the request protobuf. This method is considered an internal implementation detail and + * not meant to be used by applications. + */ + @InternalApi + public com.google.bigtable.admin.v2.AuthorizedView toProto() { + return proto; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + AuthorizedView that = (AuthorizedView) o; + return Objects.equal(proto, that.proto); + } + + @Override + public int hashCode() { + return Objects.hashCode(proto); + } + + /** + * Represents a subset of a Table. Please check the implementations of this interface for more + * details. + */ + @InternalExtensionOnly + public interface AuthorizedViewType {} +} diff --git a/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/admin/v2/models/CreateAuthorizedViewRequest.java b/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/admin/v2/models/CreateAuthorizedViewRequest.java new file mode 100644 index 0000000000..0c251fa666 --- /dev/null +++ b/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/admin/v2/models/CreateAuthorizedViewRequest.java @@ -0,0 +1,117 @@ +/* + * Copyright 2024 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.admin.v2.models; + +import com.google.api.core.InternalApi; +import com.google.cloud.bigtable.admin.v2.internal.NameUtil; +import com.google.cloud.bigtable.admin.v2.models.AuthorizedView.AuthorizedViewType; +import com.google.common.base.Objects; +import com.google.common.base.Preconditions; +import javax.annotation.Nonnull; + +/** + * Parameters for creating a new Cloud Bigtable {@link AuthorizedView}, which represents subsets of + * a particular table. + * + *

Sample code: + * + *

{@code
+ * CreateAuthorizedViewRequest request =
+ *     CreateAuthorizedViewRequest.of("my-table", "my-new-authorized-view")
+ *         .setAuthorizedViewType(
+ *             SubsetView.create()
+ *                 .addRowPrefix("row#")
+ *                 .addFamilySubsets(
+ *                     "my-family", FamilySubsets.create().addQualifier("column")));
+ * }
+ * + * @see AuthorizedView for more details. + */ +public final class CreateAuthorizedViewRequest { + private final String tableId; + private final com.google.bigtable.admin.v2.CreateAuthorizedViewRequest.Builder requestBuilder = + com.google.bigtable.admin.v2.CreateAuthorizedViewRequest.newBuilder(); + + public static CreateAuthorizedViewRequest of( + @Nonnull String tableId, @Nonnull String authorizedViewId) { + return new CreateAuthorizedViewRequest(tableId, authorizedViewId); + } + + private CreateAuthorizedViewRequest(@Nonnull String tableId, @Nonnull String authorizedViewId) { + Preconditions.checkNotNull(tableId, "tableId must be set"); + Preconditions.checkNotNull(authorizedViewId, "authorizedViewId must be set"); + + this.tableId = tableId; + requestBuilder.setAuthorizedViewId(authorizedViewId); + } + + /** Configures if the authorized view is deletion protected. */ + public CreateAuthorizedViewRequest setDeletionProtection(boolean deletionProtection) { + requestBuilder.getAuthorizedViewBuilder().setDeletionProtection(deletionProtection); + return this; + } + + /** + * Sets the implementation for this authorized view. + * + * @see AuthorizedViewType for details. + */ + public CreateAuthorizedViewRequest setAuthorizedViewType( + @Nonnull AuthorizedViewType authorizedViewType) { + Preconditions.checkNotNull(authorizedViewType, "authorizedViewType must be set"); + + if (authorizedViewType instanceof SubsetView) { + requestBuilder + .getAuthorizedViewBuilder() + .setSubsetView(((SubsetView) authorizedViewType).toProto()); + } else { + throw new IllegalArgumentException("Unknown authorizedViewType: " + authorizedViewType); + } + + return this; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + CreateAuthorizedViewRequest that = (CreateAuthorizedViewRequest) o; + return Objects.equal(requestBuilder.build(), that.requestBuilder.build()) + && Objects.equal(tableId, that.tableId); + } + + @Override + public int hashCode() { + return Objects.hashCode(requestBuilder.build(), tableId); + } + + /** + * Creates the request protobuf. This method is considered an internal implementation detail and + * not meant to be used by applications. + */ + @InternalApi + public com.google.bigtable.admin.v2.CreateAuthorizedViewRequest toProto( + @Nonnull String projectId, @Nonnull String instanceId) { + return requestBuilder + .setParent(NameUtil.formatTableName(projectId, instanceId, tableId)) + .build(); + } +} diff --git a/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/admin/v2/models/FamilySubsets.java b/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/admin/v2/models/FamilySubsets.java new file mode 100644 index 0000000000..e80452b2af --- /dev/null +++ b/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/admin/v2/models/FamilySubsets.java @@ -0,0 +1,119 @@ +/* + * Copyright 2024 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.admin.v2.models; + +import com.google.api.core.InternalApi; +import com.google.common.base.Objects; +import com.google.common.collect.ImmutableList; +import com.google.protobuf.ByteString; +import java.util.List; +import javax.annotation.Nonnull; + +/** Represents subsets of a particular column family that are included in this authorized view. */ +public final class FamilySubsets { + private final com.google.bigtable.admin.v2.AuthorizedView.FamilySubsets.Builder builder; + + /** + * Wraps the protobuf. This method is considered an internal implementation detail and not meant + * to be used by applications. + */ + @InternalApi + public static FamilySubsets fromProto( + @Nonnull com.google.bigtable.admin.v2.AuthorizedView.FamilySubsets proto) { + return new FamilySubsets(proto); + } + + public static FamilySubsets create() { + return new FamilySubsets(); + } + + private FamilySubsets(@Nonnull com.google.bigtable.admin.v2.AuthorizedView.FamilySubsets proto) { + this.builder = proto.toBuilder(); + } + + private FamilySubsets() { + this.builder = com.google.bigtable.admin.v2.AuthorizedView.FamilySubsets.newBuilder(); + } + + /** Gets the list of column qualifiers included in this authorized view. */ + public List getQualifiers() { + return ImmutableList.copyOf(this.builder.getQualifiersList()); + } + + /** Gets the list of column qualifier prefixes included in this authorized view. */ + public List getQualifierPrefixes() { + return ImmutableList.copyOf(this.builder.getQualifierPrefixesList()); + } + + /** Adds an individual column qualifier to be included in this authorized view. */ + public FamilySubsets addQualifier(ByteString qualifier) { + this.builder.addQualifiers(qualifier); + return this; + } + + /** Adds an individual column qualifier to be included in this authorized view. */ + public FamilySubsets addQualifier(String qualifier) { + this.builder.addQualifiers(ByteString.copyFromUtf8(qualifier)); + return this; + } + + /** + * Adds a prefix for column qualifiers to be included in this authorized view. Every qualifier + * starting with the prefix will be included in this authorized view. An empty string ("") prefix + * means to provide access to all qualifiers. + */ + public FamilySubsets addQualifierPrefix(ByteString qualifierPrefix) { + this.builder.addQualifierPrefixes(qualifierPrefix); + return this; + } + + /** + * Adds a prefix for column qualifiers to be included in this authorized view. Every qualifier + * starting with the prefix will be included in this authorized view. An empty string ("") prefix + * means to provide access to all qualifiers. + */ + public FamilySubsets addQualifierPrefix(String qualifierPrefix) { + this.builder.addQualifierPrefixes(ByteString.copyFromUtf8(qualifierPrefix)); + return this; + } + + /** + * Creates the request protobuf. This method is considered an internal implementation detail and + * not meant to be used by applications. + */ + @InternalApi + public com.google.bigtable.admin.v2.AuthorizedView.FamilySubsets toProto() { + return builder.build(); + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + FamilySubsets that = (FamilySubsets) o; + return Objects.equal(builder.build(), that.builder.build()); + } + + @Override + public int hashCode() { + return Objects.hashCode(builder.build()); + } +} diff --git a/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/admin/v2/models/SubsetView.java b/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/admin/v2/models/SubsetView.java new file mode 100644 index 0000000000..6ace603e46 --- /dev/null +++ b/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/admin/v2/models/SubsetView.java @@ -0,0 +1,123 @@ +/* + * Copyright 2024 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.admin.v2.models; + +import com.google.api.core.InternalApi; +import com.google.cloud.bigtable.admin.v2.models.AuthorizedView.AuthorizedViewType; +import com.google.common.base.Objects; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableMap; +import com.google.protobuf.ByteString; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; +import javax.annotation.Nonnull; + +/** + * Defines a simple authorized view that is a subset of the underlying Table. + * + *

Users can specify the rows in the form of row key prefixes, and specify the column families by + * adding the family id along with its familySubsets rule to the family subsets map. The subset is + * defined by the intersection of the specified row key prefixes and column family subsets. + */ +public class SubsetView implements AuthorizedViewType { + private final com.google.bigtable.admin.v2.AuthorizedView.SubsetView.Builder builder; + + /** + * Wraps the protobuf. This method is considered an internal implementation detail and not meant + * to be used by applications. + */ + @InternalApi + public static SubsetView fromProto( + @Nonnull com.google.bigtable.admin.v2.AuthorizedView.SubsetView proto) { + return new SubsetView(proto); + } + + public static SubsetView create() { + return new SubsetView(); + } + + private SubsetView(@Nonnull com.google.bigtable.admin.v2.AuthorizedView.SubsetView proto) { + this.builder = proto.toBuilder(); + } + + private SubsetView() { + this.builder = com.google.bigtable.admin.v2.AuthorizedView.SubsetView.newBuilder(); + } + + /** Gets the row prefixes to be included in this subset view. */ + public List getRowPrefixes() { + return ImmutableList.copyOf(this.builder.getRowPrefixesList()); + } + + /** Gets the map from familyId to familySubsets in this subset view. */ + public Map getFamilySubsets() { + ImmutableMap.Builder familySubsets = ImmutableMap.builder(); + for (Entry entry : + builder.getFamilySubsetsMap().entrySet()) { + familySubsets.put(entry.getKey(), FamilySubsets.fromProto(entry.getValue())); + } + return familySubsets.build(); + } + + /** Adds a new rowPrefix to the subset view. */ + public SubsetView addRowPrefix(ByteString rowPrefix) { + this.builder.addRowPrefixes(rowPrefix); + return this; + } + + /** Adds a new rowPrefix to the subset view. */ + public SubsetView addRowPrefix(String rowPrefix) { + this.builder.addRowPrefixes(ByteString.copyFromUtf8(rowPrefix)); + return this; + } + + /** + * Adds a new familyId with its familySubsets to the subset view. Please note that calling this + * method with the same familyId will overwrite the previous rule set on the family. + */ + public SubsetView setFamilySubsets(String familyId, FamilySubsets familySubsets) { + this.builder.putFamilySubsets(familyId, familySubsets.toProto()); + return this; + } + + /** + * Creates the request protobuf. This method is considered an internal implementation detail and + * not meant to be used by applications. + */ + @InternalApi + public com.google.bigtable.admin.v2.AuthorizedView.SubsetView toProto() { + return builder.build(); + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + SubsetView that = (SubsetView) o; + return Objects.equal(builder.build(), that.builder.build()); + } + + @Override + public int hashCode() { + return Objects.hashCode(builder.build()); + } +} diff --git a/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/admin/v2/models/UpdateAuthorizedViewRequest.java b/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/admin/v2/models/UpdateAuthorizedViewRequest.java new file mode 100644 index 0000000000..fbb54c994e --- /dev/null +++ b/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/admin/v2/models/UpdateAuthorizedViewRequest.java @@ -0,0 +1,154 @@ +/* + * Copyright 2024 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.admin.v2.models; + +import com.google.api.core.InternalApi; +import com.google.cloud.bigtable.admin.v2.internal.NameUtil; +import com.google.cloud.bigtable.admin.v2.models.AuthorizedView.AuthorizedViewType; +import com.google.common.base.Objects; +import com.google.common.base.Preconditions; +import com.google.protobuf.FieldMask; +import com.google.protobuf.util.FieldMaskUtil; +import javax.annotation.Nonnull; + +/** + * Parameters for updating an existing Cloud Bigtable {@link AuthorizedView}. + * + *

Sample code: + * + *

{@code
+ * AuthorizedView existingAuthorizedView = client.getAuthorizedView("my-table", "my-authorized-view");
+ * UpdateAuthorizedViewRequest request =
+ *     UpdateAuthorizedViewRequest.of(existingAuthorizedView).setDeletionProtection(true);
+ * }
+ * + * @see AuthorizedView for more details. + */ +public final class UpdateAuthorizedViewRequest { + private final com.google.bigtable.admin.v2.UpdateAuthorizedViewRequest.Builder requestBuilder; + private final String tableId; + private final String authorizedViewId; + + /** Builds a new update request using an existing authorized view. */ + public static UpdateAuthorizedViewRequest of(@Nonnull AuthorizedView authorizedView) { + return new UpdateAuthorizedViewRequest( + authorizedView.getTableId(), + authorizedView.getId(), + com.google.bigtable.admin.v2.UpdateAuthorizedViewRequest.newBuilder() + .setAuthorizedView(authorizedView.toProto())); + } + + /** Builds a new update authorized view request. */ + public static UpdateAuthorizedViewRequest of( + @Nonnull String tableId, @Nonnull String authorizedViewId) { + return new UpdateAuthorizedViewRequest( + tableId, + authorizedViewId, + com.google.bigtable.admin.v2.UpdateAuthorizedViewRequest.newBuilder()); + } + + private UpdateAuthorizedViewRequest( + @Nonnull String tableId, + @Nonnull String authorizedViewId, + @Nonnull com.google.bigtable.admin.v2.UpdateAuthorizedViewRequest.Builder requestBuilder) { + Preconditions.checkNotNull(tableId, "tableId must be set"); + Preconditions.checkNotNull(authorizedViewId, "authorizedViewId must be set"); + Preconditions.checkNotNull(requestBuilder, "proto builder must be set"); + + this.tableId = tableId; + this.authorizedViewId = authorizedViewId; + this.requestBuilder = requestBuilder; + } + + /** Changes the deletion protection of an existing authorized view. */ + public UpdateAuthorizedViewRequest setDeletionProtection(boolean deletionProtection) { + requestBuilder.getAuthorizedViewBuilder().setDeletionProtection(deletionProtection); + updateFieldMask(com.google.bigtable.admin.v2.AuthorizedView.DELETION_PROTECTION_FIELD_NUMBER); + return this; + } + + /** + * Updates the implementation for this authorized view. + * + * @see AuthorizedViewType for details. + */ + public UpdateAuthorizedViewRequest setAuthorizedViewType( + @Nonnull AuthorizedViewType authorizedViewType) { + Preconditions.checkNotNull(authorizedViewType, "authorizedViewType must be set"); + + if (authorizedViewType instanceof SubsetView) { + requestBuilder + .getAuthorizedViewBuilder() + .setSubsetView(((SubsetView) authorizedViewType).toProto()); + updateFieldMask(com.google.bigtable.admin.v2.AuthorizedView.SUBSET_VIEW_FIELD_NUMBER); + } else { + throw new IllegalArgumentException("Unknown authorizedViewType: " + authorizedViewType); + } + + return this; + } + + /** + * Configures if safety warnings should be disabled. If set, then updates that making the + * authorized view more restrictive are allowed. + */ + @SuppressWarnings("WeakerAccess") + public UpdateAuthorizedViewRequest setIgnoreWarnings(boolean value) { + requestBuilder.setIgnoreWarnings(value); + return this; + } + + private void updateFieldMask(int fieldNumber) { + FieldMask newMask = + FieldMaskUtil.fromFieldNumbers( + com.google.bigtable.admin.v2.AuthorizedView.class, fieldNumber); + requestBuilder.setUpdateMask(FieldMaskUtil.union(requestBuilder.getUpdateMask(), newMask)); + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + UpdateAuthorizedViewRequest that = (UpdateAuthorizedViewRequest) o; + return Objects.equal(requestBuilder.build(), that.requestBuilder.build()) + && Objects.equal(tableId, that.tableId) + && Objects.equal(authorizedViewId, that.authorizedViewId); + } + + @Override + public int hashCode() { + return Objects.hashCode(requestBuilder.build(), tableId, authorizedViewId); + } + + /** + * Creates the request protobuf. This method is considered an internal implementation detail and + * not meant to be used by applications. + */ + @InternalApi + public com.google.bigtable.admin.v2.UpdateAuthorizedViewRequest toProto( + @Nonnull String projectId, @Nonnull String instanceId) { + requestBuilder + .getAuthorizedViewBuilder() + .setName( + NameUtil.formatAuthorizedViewName(projectId, instanceId, tableId, authorizedViewId)); + return requestBuilder.build(); + } +} 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 f84a5dd098..8f08f82d8a 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 @@ -44,6 +44,9 @@ import com.google.cloud.bigtable.data.v2.models.RowAdapter; import com.google.cloud.bigtable.data.v2.models.RowMutation; import com.google.cloud.bigtable.data.v2.models.RowMutationEntry; +import com.google.cloud.bigtable.data.v2.models.SampleRowKeysRequest; +import com.google.cloud.bigtable.data.v2.models.TableId; +import com.google.cloud.bigtable.data.v2.models.TargetId; import com.google.cloud.bigtable.data.v2.stub.EnhancedBigtableStub; import com.google.common.util.concurrent.MoreExecutors; import com.google.protobuf.ByteString; @@ -206,7 +209,9 @@ static BigtableDataClient createWithClientContext( * } * * @throws com.google.api.gax.rpc.ApiException when a serverside error occurs + * @deprecated Please use {@link BigtableDataClient#exists(TargetId, String)} instead. */ + @Deprecated public boolean exists(String tableId, String rowKey) { return ApiExceptions.callAndTranslateApiException(existsAsync(tableId, rowKey)); } @@ -233,11 +238,71 @@ public boolean exists(String tableId, String rowKey) { * } * * @throws com.google.api.gax.rpc.ApiException when a serverside error occurs + * @deprecated Please use {@link BigtableDataClient#exists(TargetId, ByteString)} instead. */ + @Deprecated public boolean exists(String tableId, ByteString rowKey) { return ApiExceptions.callAndTranslateApiException(existsAsync(tableId, rowKey)); } + /** + * Confirms synchronously if given row key exists or not on the specified {@link TargetId}. + * + *

Sample code: + * + *

{@code
+   * try (BigtableDataClient bigtableDataClient = BigtableDataClient.create("[PROJECT]", "[INSTANCE]")) {
+   *   String tableId = "[TABLE]";
+   *   String key = "key";
+   *
+   *   boolean isRowPresent = bigtableDataClient.exists(TableId.of(tableId), key);
+   *
+   *   // Do something with result, for example, display a message
+   *   if(isRowPresent) {
+   *     System.out.println(key + " is present");
+   *   }
+   * } catch(ApiException e) {
+   *   e.printStackTrace();
+   * }
+   * }
+ * + * @throws com.google.api.gax.rpc.ApiException when a serverside error occurs + * @see com.google.cloud.bigtable.data.v2.models.AuthorizedViewId + * @see TableId + */ + public boolean exists(TargetId targetId, String rowKey) { + return ApiExceptions.callAndTranslateApiException(existsAsync(targetId, rowKey)); + } + + /** + * Confirms synchronously if given row key exists or not on the specified {@link TargetId}. + * + *

Sample code: + * + *

{@code
+   * try (BigtableDataClient bigtableDataClient = BigtableDataClient.create("[PROJECT]", "[INSTANCE]")) {
+   *   String tableId = "[TABLE]";
+   *   ByteString key = ByteString.copyFromUtf8("key");
+   *
+   *   boolean isRowPresent = bigtableDataClient.exists(TableId.of(tableId), key);
+   *
+   *   // Do something with result, for example, display a message
+   *   if(isRowPresent) {
+   *     System.out.println(key.toStringUtf8() + " is present");
+   *   }
+   * } catch(ApiException e) {
+   *   e.printStackTrace();
+   * }
+   * }
+ * + * @throws com.google.api.gax.rpc.ApiException when a serverside error occurs + * @see com.google.cloud.bigtable.data.v2.models.AuthorizedViewId + * @see TableId + */ + public boolean exists(TargetId targetId, ByteString rowKey) { + return ApiExceptions.callAndTranslateApiException(existsAsync(targetId, rowKey)); + } + /** * Confirms asynchronously if given row key exists or not. * @@ -262,7 +327,10 @@ public boolean exists(String tableId, ByteString rowKey) { * }, MoreExecutors.directExecutor()); * } * } + * + * @deprecated Please use {@link BigtableDataClient#existsAsync(TargetId, String)} instead. */ + @Deprecated public ApiFuture existsAsync(String tableId, String rowKey) { return existsAsync(tableId, ByteString.copyFromUtf8(rowKey)); } @@ -291,10 +359,77 @@ public ApiFuture existsAsync(String tableId, String rowKey) { * }, MoreExecutors.directExecutor()); * } * } + * + * @deprecated Please use {@link BigtableDataClient#existsAsync(TargetId, ByteString)} instead. */ + @Deprecated public ApiFuture existsAsync(String tableId, ByteString rowKey) { + return existsAsync(TableId.of(tableId), rowKey); + } + + /** + * Confirms asynchronously if given row key exists or not on the specified {@link TargetId}. + * + *

Sample code: + * + *

{@code
+   * try (BigtableDataClient bigtableDataClient = BigtableDataClient.create("[PROJECT]", "[INSTANCE]")) {
+   *   String tableId = "[TABLE]";
+   *   final String key = "key";
+   *
+   *   ApiFuture futureResult = bigtableDataClient.existsAsync(TableId.of(tableId), key);
+   *
+   *   ApiFutures.addCallback(futureResult, new ApiFutureCallback() {
+   *     public void onFailure(Throwable t) {
+   *       t.printStackTrace();
+   *     }
+   *     public void onSuccess(Boolean isRowPresent) {
+   *       if(isRowPresent) {
+   *         System.out.println(key + " is present");
+   *       }
+   *     }
+   *   }, MoreExecutors.directExecutor());
+   * }
+   * }
+ * + * @see com.google.cloud.bigtable.data.v2.models.AuthorizedViewId + * @see TableId + */ + public ApiFuture existsAsync(TargetId targetId, String rowKey) { + return existsAsync(targetId, ByteString.copyFromUtf8(rowKey)); + } + + /** + * Confirms asynchronously if given row key exists or not on the specified {@link TargetId}. + * + *

Sample code: + * + *

{@code
+   * try (BigtableDataClient bigtableDataClient = BigtableDataClient.create("[PROJECT]", "[INSTANCE]")) {
+   *   String tableId = "[TABLE]";
+   *   final ByteString key = ByteString.copyFromUtf8("key");
+   *
+   *   ApiFuture futureResult = bigtableDataClient.existsAsync(TableId.of(tableId), key);
+   *
+   *   ApiFutures.addCallback(futureResult, new ApiFutureCallback() {
+   *     public void onFailure(Throwable t) {
+   *       t.printStackTrace();
+   *     }
+   *     public void onSuccess(Boolean isRowPresent) {
+   *       if(isRowPresent) {
+   *         System.out.println(key.toStringUtf8() + " is present");
+   *       }
+   *     }
+   *   }, MoreExecutors.directExecutor());
+   * }
+   * }
+ * + * @see com.google.cloud.bigtable.data.v2.models.AuthorizedViewId + * @see TableId + */ + public ApiFuture existsAsync(TargetId targetId, ByteString rowKey) { Query query = - Query.create(tableId) + Query.create(targetId) .rowKey(rowKey) .filter( FILTERS @@ -338,7 +473,9 @@ public Boolean apply(Row row) { * } * * @throws com.google.api.gax.rpc.ApiException when a serverside error occurs + * @deprecated Please use {@link BigtableDataClient#readRow(TargetId, ByteString)} instead. */ + @Deprecated public Row readRow(String tableId, ByteString rowKey) { return ApiExceptions.callAndTranslateApiException(readRowAsync(tableId, rowKey, null)); } @@ -368,7 +505,9 @@ public Row readRow(String tableId, ByteString rowKey) { * } * * @throws com.google.api.gax.rpc.ApiException when a serverside error occurs + * @deprecated Please use {@link BigtableDataClient#readRow(TargetId, String)} instead. */ + @Deprecated public Row readRow(String tableId, String rowKey) { return ApiExceptions.callAndTranslateApiException( readRowAsync(tableId, ByteString.copyFromUtf8(rowKey), null)); @@ -404,7 +543,9 @@ public Row readRow(String tableId, String rowKey) { * } * * @throws com.google.api.gax.rpc.ApiException when a serverside error occurs + * @deprecated Please use {@link BigtableDataClient#readRow(TargetId, String, Filter)} instead. */ + @Deprecated public Row readRow(String tableId, String rowKey, @Nullable Filter filter) { return ApiExceptions.callAndTranslateApiException( readRowAsync(tableId, ByteString.copyFromUtf8(rowKey), filter)); @@ -440,11 +581,154 @@ public Row readRow(String tableId, String rowKey, @Nullable Filter filter) { * } * * @throws com.google.api.gax.rpc.ApiException when a serverside error occurs + * @deprecated Please use {@link BigtableDataClient#readRow(TargetId, ByteString, Filter)} + * instead. */ + @Deprecated public Row readRow(String tableId, ByteString rowKey, @Nullable Filter filter) { return ApiExceptions.callAndTranslateApiException(readRowAsync(tableId, rowKey, filter)); } + /** + * Convenience method for synchronously reading a single row on the specified {@link TargetId}. If + * the row does not exist, the value will be null. + * + *

Sample code: + * + *

{@code
+   * try (BigtableDataClient bigtableDataClient = BigtableDataClient.create("[PROJECT]", "[INSTANCE]")) {
+   *   String tableId = "[TABLE]";
+   *
+   *   Row row = bigtableDataClient.readRow(TableId.of(tableId), ByteString.copyFromUtf8("key"));
+   *   // Do something with row, for example, display all cells
+   *   if(row != null) {
+   *     System.out.println(row.getKey().toStringUtf8());
+   *      for(RowCell cell : row.getCells()) {
+   *        System.out.printf("Family: %s   Qualifier: %s   Value: %s", cell.getFamily(),
+   *           cell.getQualifier().toStringUtf8(), cell.getValue().toStringUtf8());
+   *      }
+   *   }
+   * } catch(ApiException e) {
+   *   e.printStackTrace();
+   * }
+   * }
+ * + * @throws com.google.api.gax.rpc.ApiException when a serverside error occurs + * @see com.google.cloud.bigtable.data.v2.models.AuthorizedViewId + * @see TableId + */ + public Row readRow(TargetId targetId, ByteString rowKey) { + return ApiExceptions.callAndTranslateApiException(readRowAsync(targetId, rowKey, null)); + } + + /** + * Convenience method for synchronously reading a single row on the specified {@link TargetId}. If + * the row does not exist, the value will be null. + * + *

Sample code: + * + *

{@code
+   * try (BigtableDataClient bigtableDataClient = BigtableDataClient.create("[PROJECT]", "[INSTANCE]")) {
+   *   String tableId = "[TABLE]";
+   *
+   *   Row row = bigtableDataClient.readRow(TableId.of(tableId), "key");
+   *   // Do something with row, for example, display all cells
+   *   if(row != null) {
+   *     System.out.println(row.getKey().toStringUtf8());
+   *      for(RowCell cell : row.getCells()) {
+   *        System.out.printf("Family: %s   Qualifier: %s   Value: %s", cell.getFamily(),
+   *           cell.getQualifier().toStringUtf8(), cell.getValue().toStringUtf8());
+   *      }
+   *   }
+   * } catch(ApiException e) {
+   *   e.printStackTrace();
+   * }
+   * }
+ * + * @throws com.google.api.gax.rpc.ApiException when a serverside error occurs + * @see com.google.cloud.bigtable.data.v2.models.AuthorizedViewId + * @see TableId + */ + public Row readRow(TargetId targetId, String rowKey) { + return ApiExceptions.callAndTranslateApiException( + readRowAsync(targetId, ByteString.copyFromUtf8(rowKey), null)); + } + + /** + * Convenience method for synchronously reading a single row on the specified {@link TargetId}. If + * the row does not exist, the value will be null. + * + *

Sample code: + * + *

{@code
+   * try (BigtableDataClient bigtableDataClient = BigtableDataClient.create("[PROJECT]", "[INSTANCE]")) {
+   *   String tableId = "[TABLE]";
+   *
+   *   // Build the filter expression
+   *   Filter filter = FILTERS.chain()
+   *         .filter(FILTERS.qualifier().regex("prefix.*"))
+   *         .filter(FILTERS.limit().cellsPerRow(10));
+   *
+   *   Row row = bigtableDataClient.readRow(TableId.of(tableId), "key", filter);
+   *   // Do something with row, for example, display all cells
+   *   if(row != null) {
+   *     System.out.println(row.getKey().toStringUtf8());
+   *      for(RowCell cell : row.getCells()) {
+   *        System.out.printf("Family: %s   Qualifier: %s   Value: %s", cell.getFamily(),
+   *           cell.getQualifier().toStringUtf8(), cell.getValue().toStringUtf8());
+   *      }
+   *   }
+   * } catch(ApiException e) {
+   *   e.printStackTrace();
+   * }
+   * }
+ * + * @throws com.google.api.gax.rpc.ApiException when a serverside error occurs + * @see com.google.cloud.bigtable.data.v2.models.AuthorizedViewId + * @see TableId + */ + public Row readRow(TargetId targetId, String rowKey, @Nullable Filter filter) { + return ApiExceptions.callAndTranslateApiException( + readRowAsync(targetId, ByteString.copyFromUtf8(rowKey), filter)); + } + + /** + * Convenience method for synchronously reading a single row on the specified {@link TargetId}. If + * the row does not exist, the value will be null. + * + *

Sample code: + * + *

{@code
+   * try (BigtableDataClient bigtableDataClient = BigtableDataClient.create("[PROJECT]", "[INSTANCE]")) {
+   *   String tableId = "[TABLE]";
+   *
+   *   // Build the filter expression
+   *   Filter filter = FILTERS.chain()
+   *         .filter(FILTERS.qualifier().regex("prefix.*"))
+   *         .filter(FILTERS.limit().cellsPerRow(10));
+   *
+   *   Row row = bigtableDataClient.readRow(TableId.of(tableId), ByteString.copyFromUtf8("key"), filter);
+   *   // Do something with row, for example, display all cells
+   *   if(row != null) {
+   *     System.out.println(row.getKey().toStringUtf8());
+   *      for(RowCell cell : row.getCells()) {
+   *        System.out.printf("Family: %s   Qualifier: %s   Value: %s", cell.getFamily(),
+   *           cell.getQualifier().toStringUtf8(), cell.getValue().toStringUtf8());
+   *      }
+   *   }
+   * } catch(ApiException e) {
+   *   e.printStackTrace();
+   * }
+   * }
+ * + * @throws com.google.api.gax.rpc.ApiException when a serverside error occurs + * @see com.google.cloud.bigtable.data.v2.models.AuthorizedViewId + * @see TableId + */ + public Row readRow(TargetId targetId, ByteString rowKey, @Nullable Filter filter) { + return ApiExceptions.callAndTranslateApiException(readRowAsync(targetId, rowKey, filter)); + } + /** * Convenience method for asynchronously reading a single row. If the row does not exist, the * future's value will be null. @@ -473,7 +757,10 @@ public Row readRow(String tableId, ByteString rowKey, @Nullable Filter filter) { * }, MoreExecutors.directExecutor()); * } * } + * + * @deprecated Please use {@link BigtableDataClient#readRowAsync(TargetId, String)} instead. */ + @Deprecated public ApiFuture readRowAsync(String tableId, String rowKey) { return readRowAsync(tableId, ByteString.copyFromUtf8(rowKey), null); } @@ -506,7 +793,10 @@ public ApiFuture readRowAsync(String tableId, String rowKey) { * }, MoreExecutors.directExecutor()); * } * } + * + * @deprecated Please use {@link BigtableDataClient#readRowAsync(TargetId, ByteString)} instead. */ + @Deprecated public ApiFuture readRowAsync(String tableId, ByteString rowKey) { return readRowAsync(tableId, rowKey, null); } @@ -544,7 +834,11 @@ public ApiFuture readRowAsync(String tableId, ByteString rowKey) { * }, MoreExecutors.directExecutor()); * } * } + * + * @deprecated Please use {@link BigtableDataClient#readRowAsync(TargetId, String, Filter)} + * instead. */ + @Deprecated public ApiFuture readRowAsync(String tableId, String rowKey, @Nullable Filter filter) { return readRowAsync(tableId, ByteString.copyFromUtf8(rowKey), filter); } @@ -582,9 +876,168 @@ public ApiFuture readRowAsync(String tableId, String rowKey, @Nullable Filt * }, MoreExecutors.directExecutor()); * } * } + * + * @deprecated Please use {@link BigtableDataClient#readRowAsync(TargetId, ByteString, Filter)} + * instead. */ + @Deprecated public ApiFuture readRowAsync(String tableId, ByteString rowKey, @Nullable Filter filter) { - Query query = Query.create(tableId).rowKey(rowKey); + return readRowAsync(TableId.of(tableId), rowKey, filter); + } + + /** + * Convenience method for asynchronously reading a single row on the specified {@link TargetId}. + * If the row does not exist, the future's value will be null. + * + *

Sample code: + * + *

{@code
+   * try (BigtableDataClient bigtableDataClient = BigtableDataClient.create("[PROJECT]", "[INSTANCE]")) {
+   *   String tableId = "[TABLE]";
+   *
+   *   ApiFuture futureResult = bigtableDataClient.readRowAsync(TableId.of(tableId), "key");
+   *
+   *   ApiFutures.addCallback(futureResult, new ApiFutureCallback() {
+   *     public void onFailure(Throwable t) {
+   *       if (t instanceof NotFoundException) {
+   *         System.out.println("Tried to read a non-existent table");
+   *       } else {
+   *         t.printStackTrace();
+   *       }
+   *     }
+   *     public void onSuccess(Row result) {
+   *       if (result != null) {
+   *          System.out.println("Got row: " + result);
+   *       }
+   *     }
+   *   }, MoreExecutors.directExecutor());
+   * }
+   * }
+ * + * @see com.google.cloud.bigtable.data.v2.models.AuthorizedViewId + * @see TableId + */ + public ApiFuture readRowAsync(TargetId targetId, String rowKey) { + return readRowAsync(targetId, ByteString.copyFromUtf8(rowKey), null); + } + + /** + * Convenience method for asynchronously reading a single row on the specified {@link TargetId}. + * If the row does not exist, the future's value will be null. + * + *

Sample code: + * + *

{@code
+   * try (BigtableDataClient bigtableDataClient = BigtableDataClient.create("[PROJECT]", "[INSTANCE]")) {
+   *   String tableId = "[TABLE]";
+   *
+   *   ApiFuture futureResult = bigtableDataClient.readRowAsync(TableId.of(tableId), ByteString.copyFromUtf8("key"));
+   *
+   *   ApiFutures.addCallback(futureResult, new ApiFutureCallback() {
+   *     public void onFailure(Throwable t) {
+   *       if (t instanceof NotFoundException) {
+   *         System.out.println("Tried to read a non-existent table");
+   *       } else {
+   *         t.printStackTrace();
+   *       }
+   *     }
+   *     public void onSuccess(Row result) {
+   *       if (result != null) {
+   *          System.out.println("Got row: " + result);
+   *       }
+   *     }
+   *   }, MoreExecutors.directExecutor());
+   * }
+   * }
+ * + * @see com.google.cloud.bigtable.data.v2.models.AuthorizedViewId + * @see TableId + */ + public ApiFuture readRowAsync(TargetId targetId, ByteString rowKey) { + return readRowAsync(targetId, rowKey, null); + } + + /** + * Convenience method for asynchronously reading a single row on the specified {@link TargetId}. + * If the row does not exist, the future's value will be null. + * + *

Sample code: + * + *

{@code
+   * try (BigtableDataClient bigtableDataClient = BigtableDataClient.create("[PROJECT]", "[INSTANCE]")) {
+   *   String tableId = "[TABLE]";
+   *
+   *   // Build the filter expression
+   *   Filters.Filter filter = FILTERS.chain()
+   *         .filter(FILTERS.qualifier().regex("prefix.*"))
+   *         .filter(FILTERS.limit().cellsPerRow(10));
+   *
+   *   ApiFuture futureResult = bigtableDataClient.readRowAsync(TableId.of(tableId), "key", filter);
+   *
+   *   ApiFutures.addCallback(futureResult, new ApiFutureCallback() {
+   *     public void onFailure(Throwable t) {
+   *       if (t instanceof NotFoundException) {
+   *         System.out.println("Tried to read a non-existent table");
+   *       } else {
+   *         t.printStackTrace();
+   *       }
+   *     }
+   *     public void onSuccess(Row result) {
+   *       if (result != null) {
+   *          System.out.println("Got row: " + result);
+   *       }
+   *     }
+   *   }, MoreExecutors.directExecutor());
+   * }
+   * }
+ * + * @see com.google.cloud.bigtable.data.v2.models.AuthorizedViewId + * @see TableId + */ + public ApiFuture readRowAsync(TargetId targetId, String rowKey, @Nullable Filter filter) { + return readRowAsync(targetId, ByteString.copyFromUtf8(rowKey), filter); + } + + /** + * Convenience method for asynchronously reading a single row on the specified {@link TargetId}. + * If the row does not exist, the value will be null. + * + *

Sample code: + * + *

{@code
+   * try (BigtableDataClient bigtableDataClient = BigtableDataClient.create("[PROJECT]", "[INSTANCE]")) {
+   *   String tableId = "[TABLE]";
+   *
+   *   // Build the filter expression
+   *   Filters.Filter filter = FILTERS.chain()
+   *         .filter(FILTERS.qualifier().regex("prefix.*"))
+   *         .filter(FILTERS.limit().cellsPerRow(10));
+   *
+   *   ApiFuture futureResult = bigtableDataClient.readRowAsync(TableId.of(tableId), ByteString.copyFromUtf8("key"), filter);
+   *
+   *   ApiFutures.addCallback(futureResult, new ApiFutureCallback() {
+   *     public void onFailure(Throwable t) {
+   *       if (t instanceof NotFoundException) {
+   *         System.out.println("Tried to read a non-existent table");
+   *       } else {
+   *         t.printStackTrace();
+   *       }
+   *     }
+   *     public void onSuccess(Row result) {
+   *       if (result != null) {
+   *          System.out.println("Got row: " + result);
+   *       }
+   *     }
+   *   }, MoreExecutors.directExecutor());
+   * }
+   * }
+ * + * @see com.google.cloud.bigtable.data.v2.models.AuthorizedViewId + * @see TableId + */ + public ApiFuture readRowAsync( + TargetId targetId, ByteString rowKey, @Nullable Filter filter) { + Query query = Query.create(targetId).rowKey(rowKey); if (filter != null) { query = query.filter(filter); } @@ -868,7 +1321,37 @@ public ServerStreamingCallable readRowsCallable(RowAdapter keyOffsets = bigtableDataClient.sampleRowKeys(tableId); + * List keyOffsets = bigtableDataClient.sampleRowKeys(tableId); + * for(KeyOffset keyOffset : keyOffsets) { + * // Do something with keyOffset + * } + * } catch(ApiException e) { + * e.printStackTrace(); + * } + * } + * + * @throws com.google.api.gax.rpc.ApiException when a serverside error occurs + * @deprecated Please use {@link BigtableDataClient#sampleRowKeys(TargetId)} instead. + */ + @Deprecated + public List sampleRowKeys(String tableId) { + return ApiExceptions.callAndTranslateApiException(sampleRowKeysAsync(tableId)); + } + + /** + * Convenience method to synchronously return a sample of row keys on the specified {@link + * TargetId}. + * + *

The returned row keys will delimit contiguous sections of the table of approximately equal + * size, which can be used to break up the data for distributed tasks like mapreduces. + * + *

Sample code: + * + *

{@code
+   * try (BigtableDataClient bigtableDataClient = BigtableDataClient.create("[PROJECT]", "[INSTANCE]")) {
+   *   String tableId = "[TABLE_ID]";
+   *
+   *   List keyOffsets = bigtableDataClient.sampleRowKeys(TableId.of(tableId));
    *   for(KeyOffset keyOffset : keyOffsets) {
    *   // Do something with keyOffset
    *   }
@@ -879,8 +1362,8 @@ public  ServerStreamingCallable readRowsCallable(RowAdapter sampleRowKeys(String tableId) {
-    return ApiExceptions.callAndTranslateApiException(sampleRowKeysAsync(tableId));
+  public List sampleRowKeys(TargetId targetId) {
+    return ApiExceptions.callAndTranslateApiException(sampleRowKeysAsync(targetId));
   }
 
   /**
@@ -908,9 +1391,48 @@ public List sampleRowKeys(String tableId) {
    *   }, MoreExecutors.directExecutor());
    * }
    * }
+ * + * @deprecated Please use {@link BigtableDataClient#sampleRowKeysAsync(TargetId)} instead. */ + @Deprecated public ApiFuture> sampleRowKeysAsync(String tableId) { - return sampleRowKeysCallable().futureCall(tableId); + return sampleRowKeysAsync(TableId.of(tableId)); + } + + /** + * Convenience method to asynchronously return a sample of row keys on the specified {@link + * TargetId}. + * + *

The returned row keys will delimit contiguous sections of the table of approximately equal + * size, which can be used to break up the data for distributed tasks like mapreduces. + * + *

Sample code: + * + *

{@code
+   * try (BigtableClient bigtableDataClient = BigtableClient.create("[PROJECT]", "[INSTANCE]")) {
+   *   String tableId = "[TABLE_ID]";
+   *   ApiFuture> keyOffsetsFuture = bigtableClient.sampleRowKeysAsync(TableId.of(tableId));
+   *
+   *   ApiFutures.addCallback(keyOffsetsFuture, new ApiFutureCallback>() {
+   *     public void onFailure(Throwable t) {
+   *       if (t instanceof NotFoundException) {
+   *         System.out.println("Tried to sample keys of a non-existent table");
+   *       } else {
+   *         t.printStackTrace();
+   *       }
+   *     }
+   *     public void onSuccess(List keyOffsets) {
+   *       System.out.println("Got key offsets: " + keyOffsets);
+   *     }
+   *   }, MoreExecutors.directExecutor());
+   * }
+   * }
+ * + * @see com.google.cloud.bigtable.data.v2.models.AuthorizedViewId + * @see TableId + */ + public ApiFuture> sampleRowKeysAsync(TargetId targetId) { + return sampleRowKeysCallableWithRequest().futureCall(SampleRowKeysRequest.create(targetId)); } /** @@ -949,11 +1471,62 @@ public ApiFuture> sampleRowKeysAsync(String tableId) { * }, MoreExecutors.directExecutor()); * } * } + * + * @deprecated Please use {@link BigtableDataClient#sampleRowKeysCallableWithRequest()} instead. */ + @Deprecated public UnaryCallable> sampleRowKeysCallable() { return stub.sampleRowKeysCallable(); } + /** + * Returns a sample of row keys in the table. This callable allows takes a {@link + * SampleRowKeysRequest} object rather than a String input, and thus can be used to sample against + * a specified {@link TargetId}. + * + *

The returned row keys will delimit contiguous sections of the table of approximately equal + * size, which can be used to break up the data for distributed tasks like mapreduces. The + * returned callable object allows for customization of api invocation. + * + *

Sample code: + * + *

{@code
+   * try (BigtableDataClient bigtableDataClient = BigtableDataClient.create("[PROJECT]", "[INSTANCE]")) {
+   *   SampleRowKeysRequest sampleRowKeys = SampleRowKeysRequest.create(TableId.of("[TABLE]"));
+   *   // Synchronous invocation
+   *   try {
+   *     List keyOffsets = bigtableDataClient.sampleRowKeysCallableWithRequest().call(sampleRowKeys);
+   *   } catch (NotFoundException e) {
+   *     System.out.println("Tried to sample keys of a non-existent table");
+   *   } catch (RuntimeException e) {
+   *     e.printStackTrace();
+   *   }
+   *
+   *   // Asynchronous invocation
+   *   ApiFuture> keyOffsetsFuture = bigtableDataClient.sampleRowKeysCallableWithRequest().futureCall(sampleRowKeys);
+   *
+   *   ApiFutures.addCallback(keyOffsetsFuture, new ApiFutureCallback>() {
+   *     public void onFailure(Throwable t) {
+   *       if (t instanceof NotFoundException) {
+   *         System.out.println("Tried to sample keys of a non-existent table");
+   *       } else {
+   *         t.printStackTrace();
+   *       }
+   *     }
+   *     public void onSuccess(List keyOffsets) {
+   *       System.out.println("Got key offsets: " + keyOffsets);
+   *     }
+   *   }, MoreExecutors.directExecutor());
+   * }
+   * }
+ * + * @see com.google.cloud.bigtable.data.v2.models.AuthorizedViewId + * @see TableId + */ + public UnaryCallable> sampleRowKeysCallableWithRequest() { + return stub.sampleRowKeysCallableWithRequest(); + } + /** * Convenience method to synchronously mutate a single row atomically. Cells already present in * the row are left unchanged unless explicitly changed by the {@link RowMutation}. @@ -1087,8 +1660,10 @@ public void bulkMutateRows(BulkMutation mutation) { * // Before `batcher` is closed, all remaining(If any) mutations are applied. * } * } + * + * @deprecated Please use {@link BigtableDataClient#newBulkMutationBatcher(TargetId)} instead. */ - @BetaApi("This surface is likely to change as the batching surface evolves.") + @Deprecated public Batcher newBulkMutationBatcher(@Nonnull String tableId) { return newBulkMutationBatcher(tableId, null); } @@ -1119,13 +1694,89 @@ public Batcher newBulkMutationBatcher(@Nonnull String ta * // Before `batcher` is closed, all remaining(If any) mutations are applied. * } * } + * + * @deprecated Please use {@link BigtableDataClient#newBulkMutationBatcher(TargetId, + * GrpcCallContext)} instead. */ + @Deprecated @BetaApi("This surface is likely to change as the batching surface evolves.") public Batcher newBulkMutationBatcher( @Nonnull String tableId, @Nullable GrpcCallContext ctx) { return stub.newMutateRowsBatcher(tableId, ctx); } + /** + * Mutates multiple rows in a batch on the specified {@link TargetId}. + * + *

Each individual row is mutated atomically as in MutateRow, but the entire batch is not + * executed atomically. The returned Batcher instance is not threadsafe, it can only be used from + * single thread. + * + *

Sample Code: + * + *

{@code
+   * try (BigtableDataClient bigtableDataClient = BigtableDataClient.create("[PROJECT]", "[INSTANCE]")) {
+   *   try (Batcher batcher = bigtableDataClient.newBulkMutationBatcher(TableId.of("[TABLE]"))) {
+   *     for (String someValue : someCollection) {
+   *       ApiFuture entryFuture =
+   *           batcher.add(
+   *               RowMutationEntry.create("[ROW KEY]")
+   *                   .setCell("[FAMILY NAME]", "[QUALIFIER]", "[VALUE]"));
+   *     }
+   *
+   *     // Blocks until mutations are applied on all submitted row entries.
+   *     batcher.flush();
+   *   }
+   *   // Before `batcher` is closed, all remaining(If any) mutations are applied.
+   * }
+   * }
+ * + * @see com.google.cloud.bigtable.data.v2.models.AuthorizedViewId + * @see TableId + */ + @BetaApi("This surface is likely to change as the batching surface evolves.") + public Batcher newBulkMutationBatcher(TargetId targetId) { + return newBulkMutationBatcher(targetId, null); + } + + /** + * Mutates multiple rows in a batch on the specified {@link TargetId}. + * + *

Each individual row is mutated atomically as in MutateRow, but the entire batch is not + * executed atomically. The returned Batcher instance is not threadsafe, it can only be used from + * single thread. This method allows customization of the underlying RPCs by passing in a {@link + * com.google.api.gax.grpc.GrpcCallContext}. The same context will be reused for all batches. This + * can be used to customize things like per attempt timeouts. + * + *

Sample Code: + * + *

{@code
+   * try (BigtableDataClient bigtableDataClient = BigtableDataClient.create("[PROJECT]", "[INSTANCE]")) {
+   *   GrpcCallContext ctx = GrpcCallContext.createDefault().withTimeout(Duration.ofSeconds(10));
+   *   try (Batcher batcher = bigtableDataClient.newBulkMutationBatcher(TableId.of("[TABLE]"), ctx)) {
+   *     for (String someValue : someCollection) {
+   *       ApiFuture entryFuture =
+   *           batcher.add(
+   *               RowMutationEntry.create("[ROW KEY]")
+   *                   .setCell("[FAMILY NAME]", "[QUALIFIER]", "[VALUE]"));
+   *     }
+   *
+   *     // Blocks until mutations are applied on all submitted row entries.
+   *     batcher.flush();
+   *   }
+   *   // Before `batcher` is closed, all remaining(If any) mutations are applied.
+   * }
+   * }
+ * + * @see com.google.cloud.bigtable.data.v2.models.AuthorizedViewId + * @see TableId + */ + @BetaApi("This surface is likely to change as the batching surface evolves.") + public Batcher newBulkMutationBatcher( + TargetId targetId, @Nullable GrpcCallContext ctx) { + return stub.newMutateRowsBatcher(targetId, ctx); + } + /** * Reads rows for given tableId in a batch. If the row does not exist, the value will be null. The * returned Batcher instance is not threadsafe, it can only be used from a single thread. @@ -1160,7 +1811,10 @@ public Batcher newBulkMutationBatcher( * List actualRows = ApiFutures.allAsList(rows).get(); * } * } + * + * @deprecated Please use {@link BigtableDataClient#newBulkReadRowsBatcher(TargetId)} instead. */ + @Deprecated public Batcher newBulkReadRowsBatcher(String tableId) { return newBulkReadRowsBatcher(tableId, null); } @@ -1206,7 +1860,11 @@ public Batcher newBulkReadRowsBatcher(String tableId) { * List actualRows = ApiFutures.allAsList(rows).get(); * } * } + * + * @deprecated Please use {@link BigtableDataClient#newBulkReadRowsBatcher(TargetId, Filter)} + * instead. */ + @Deprecated public Batcher newBulkReadRowsBatcher( String tableId, @Nullable Filters.Filter filter) { return newBulkReadRowsBatcher(tableId, filter, null); @@ -1256,12 +1914,167 @@ public Batcher newBulkReadRowsBatcher( * List actualRows = ApiFutures.allAsList(rows).get(); * } * } + * + * @deprecated Please use {@link BigtableDataClient#newBulkReadRowsBatcher(TargetId, Filter, + * GrpcCallContext)} instead. */ + @Deprecated public Batcher newBulkReadRowsBatcher( String tableId, @Nullable Filters.Filter filter, @Nullable GrpcCallContext ctx) { - Query query = Query.create(tableId); + return newBulkReadRowsBatcher(TableId.of(tableId), filter, ctx); + } + + /** + * Reads rows for given tableId in a batch on the specified {@link TargetId}. + * + *

If the row does not exist, the value will be null. The returned Batcher instance is not + * threadsafe, it can only be used from a single thread. + * + *

Performance notice: The ReadRows protocol requires that rows are sent in ascending key + * order, which means that the keys are processed sequentially on the server-side, so batching + * allows improving throughput but not latency. Lower latencies can be achieved by sending smaller + * requests concurrently. + * + *

Sample Code: + * + *

{@code
+   * try (BigtableDataClient bigtableDataClient = BigtableDataClient.create("[PROJECT]", "[INSTANCE]")) {
+   *
+   *   List> rows = new ArrayList<>();
+   *
+   *   try (Batcher batcher = bigtableDataClient.newBulkReadRowsBatcher(TableId.of("[TABLE]"))) {
+   *     for (String someValue : someCollection) {
+   *       ApiFuture rowFuture =
+   *           batcher.add(ByteString.copyFromUtf8("[ROW KEY]"));
+   *       rows.add(rowFuture);
+   *     }
+   *
+   *     // [Optional] Sends collected elements for batching asynchronously.
+   *     batcher.sendOutstanding();
+   *
+   *     // [Optional] Invokes sendOutstanding() and awaits until all pending entries are resolved.
+   *     batcher.flush();
+   *   }
+   *   // batcher.close() invokes `flush()` which will in turn invoke `sendOutstanding()` with await for
+   *   pending batches until its resolved.
+   *
+   *   List actualRows = ApiFutures.allAsList(rows).get();
+   * }
+   * }
+ * + * @see com.google.cloud.bigtable.data.v2.models.AuthorizedViewId + * @see TableId + */ + public Batcher newBulkReadRowsBatcher(TargetId targetId) { + return newBulkReadRowsBatcher(targetId, null); + } + + /** + * Reads rows for given tableId and filter criteria in a batch on the specified {@link TargetId}. + * + *

If the row does not exist, the value will be null. The returned Batcher instance is not + * threadsafe, it can only be used from a single thread. + * + *

Performance notice: The ReadRows protocol requires that rows are sent in ascending key + * order, which means that the keys are processed sequentially on the server-side, so batching + * allows improving throughput but not latency. Lower latencies can be achieved by sending smaller + * requests concurrently. + * + *

Sample Code: + * + *

{@code
+   * try (BigtableDataClient bigtableDataClient = BigtableDataClient.create("[PROJECT]", "[INSTANCE]")) {
+   *
+   *  // Build the filter expression
+   *  Filter filter = FILTERS.chain()
+   *         .filter(FILTERS.key().regex("prefix.*"))
+   *         .filter(FILTERS.limit().cellsPerRow(10));
+   *
+   *   List> rows = new ArrayList<>();
+   *
+   *   try (Batcher batcher = bigtableDataClient.newBulkReadRowsBatcher(TableId.of("[TABLE]"), filter)) {
+   *     for (String someValue : someCollection) {
+   *       ApiFuture rowFuture =
+   *           batcher.add(ByteString.copyFromUtf8("[ROW KEY]"));
+   *       rows.add(rowFuture);
+   *     }
+   *
+   *     // [Optional] Sends collected elements for batching asynchronously.
+   *     batcher.sendOutstanding();
+   *
+   *     // [Optional] Invokes sendOutstanding() and awaits until all pending entries are resolved.
+   *     batcher.flush();
+   *   }
+   *   // batcher.close() invokes `flush()` which will in turn invoke `sendOutstanding()` with await for
+   *   pending batches until its resolved.
+   *
+   *   List actualRows = ApiFutures.allAsList(rows).get();
+   * }
+   * }
+ * + * @see com.google.cloud.bigtable.data.v2.models.AuthorizedViewId + * @see TableId + */ + public Batcher newBulkReadRowsBatcher( + TargetId targetId, @Nullable Filter filter) { + return newBulkReadRowsBatcher(targetId, filter, null); + } + + /** + * Reads rows for given tableId and filter criteria in a batch on the specified {@link TargetId}. + * + *

If the row does not exist, the value will be null. The returned Batcher instance is not + * threadsafe, it can only be used from a single thread. This method allows customization of the + * underlying RPCs by passing in a {@link com.google.api.gax.grpc.GrpcCallContext}. The same + * context will be reused for all batches. This can be used to customize things like per attempt + * timeouts. + * + *

Performance notice: The ReadRows protocol requires that rows are sent in ascending key + * order, which means that the keys are processed sequentially on the server-side, so batching + * allows improving throughput but not latency. Lower latencies can be achieved by sending smaller + * requests concurrently. + * + *

Sample Code: + * + *

{@code
+   * try (BigtableDataClient bigtableDataClient = BigtableDataClient.create("[PROJECT]", "[INSTANCE]")) {
+   *
+   *  // Build the filter expression
+   *  Filter filter = FILTERS.chain()
+   *         .filter(FILTERS.key().regex("prefix.*"))
+   *         .filter(FILTERS.limit().cellsPerRow(10));
+   *
+   *   List> rows = new ArrayList<>();
+   *
+   *   try (Batcher batcher = bigtableDataClient.newBulkReadRowsBatcher(
+   *    TableId.of("[TABLE]"), filter, GrpcCallContext.createDefault().withTimeout(Duration.ofSeconds(10)))) {
+   *     for (String someValue : someCollection) {
+   *       ApiFuture rowFuture =
+   *           batcher.add(ByteString.copyFromUtf8("[ROW KEY]"));
+   *       rows.add(rowFuture);
+   *     }
+   *
+   *     // [Optional] Sends collected elements for batching asynchronously.
+   *     batcher.sendOutstanding();
+   *
+   *     // [Optional] Invokes sendOutstanding() and awaits until all pending entries are resolved.
+   *     batcher.flush();
+   *   }
+   *   // batcher.close() invokes `flush()` which will in turn invoke `sendOutstanding()` with await for
+   *   pending batches until its resolved.
+   *
+   *   List actualRows = ApiFutures.allAsList(rows).get();
+   * }
+   * }
+ * + * @see com.google.cloud.bigtable.data.v2.models.AuthorizedViewId + * @see TableId + */ + public Batcher newBulkReadRowsBatcher( + TargetId targetId, @Nullable Filter filter, @Nullable GrpcCallContext ctx) { + Query query = Query.create(targetId); if (filter != null) { - query.filter(filter); + query = query.filter(filter); } return stub.newBulkReadRowsBatcher(query, ctx); } diff --git a/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/internal/NameUtil.java b/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/internal/NameUtil.java index 4744d3ef1e..68c66067b1 100644 --- a/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/internal/NameUtil.java +++ b/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/internal/NameUtil.java @@ -16,6 +16,9 @@ package com.google.cloud.bigtable.data.v2.internal; import com.google.api.core.InternalApi; +import com.google.cloud.bigtable.data.v2.models.AuthorizedViewId; +import com.google.cloud.bigtable.data.v2.models.TableId; +import com.google.cloud.bigtable.data.v2.models.TargetId; import java.util.regex.Matcher; import java.util.regex.Pattern; import javax.annotation.Nonnull; @@ -30,6 +33,8 @@ public class NameUtil { private static final Pattern TABLE_PATTERN = Pattern.compile("projects/([^/]+)/instances/([^/]+)/tables/([^/]+)"); + private static final Pattern AUTHORIZED_VIEW_PATTERN = + Pattern.compile("projects/([^/]+)/instances/([^/]+)/tables/([^/]+)/authorizedViews/([^/]+)"); public static String formatInstanceName(@Nonnull String projectId, @Nonnull String instanceId) { return "projects/" + projectId + "/instances/" + instanceId; @@ -40,6 +45,14 @@ public static String formatTableName( return formatInstanceName(projectId, instanceId) + "/tables/" + tableId; } + public static String formatAuthorizedViewName( + @Nonnull String projectId, + @Nonnull String instanceId, + @Nonnull String tableId, + @Nonnull String authorizedViewId) { + return formatTableName(projectId, instanceId, tableId) + "/authorizedViews/" + authorizedViewId; + } + public static String extractTableIdFromTableName(@Nonnull String fullTableName) { Matcher matcher = TABLE_PATTERN.matcher(fullTableName); if (!matcher.matches()) { @@ -47,4 +60,59 @@ public static String extractTableIdFromTableName(@Nonnull String fullTableName) } return matcher.group(3); } + + public static String extractTableIdFromAuthorizedViewName( + @Nonnull String fullAuthorizedViewName) { + Matcher matcher = AUTHORIZED_VIEW_PATTERN.matcher(fullAuthorizedViewName); + if (!matcher.matches()) { + throw new IllegalArgumentException("Invalid authorized view name: " + fullAuthorizedViewName); + } + return matcher.group(3); + } + + public static String extractTableNameFromAuthorizedViewName( + @Nonnull String fullAuthorizedViewName) { + Matcher matcher = AUTHORIZED_VIEW_PATTERN.matcher(fullAuthorizedViewName); + if (!matcher.matches()) { + throw new IllegalArgumentException("Invalid authorized view name: " + fullAuthorizedViewName); + } + return formatTableName(matcher.group(1), matcher.group(2), matcher.group(3)); + } + + public static String extractAuthorizedViewIdFromAuthorizedViewName( + @Nonnull String fullAuthorizedViewName) { + Matcher matcher = AUTHORIZED_VIEW_PATTERN.matcher(fullAuthorizedViewName); + if (!matcher.matches()) { + throw new IllegalArgumentException("Invalid authorized view name: " + fullAuthorizedViewName); + } + return matcher.group(4); + } + + /** A helper to convert fully qualified tableName and authorizedViewName to a {@link TargetId} */ + public static TargetId extractTargetId( + @Nonnull String tableName, @Nonnull String authorizedViewName) { + if (tableName.isEmpty() && authorizedViewName.isEmpty()) { + throw new IllegalArgumentException( + "Either table name or authorized view name must be specified. Table name: " + + tableName + + ", authorized view name: " + + authorizedViewName); + } + if (!tableName.isEmpty() && !authorizedViewName.isEmpty()) { + throw new IllegalArgumentException( + "Table name and authorized view name cannot be specified at the same time. Table name: " + + tableName + + ", authorized view name: " + + authorizedViewName); + } + + if (!tableName.isEmpty()) { + String tableId = extractTableIdFromTableName(tableName); + return TableId.of(tableId); + } else { + String tableId = extractTableIdFromAuthorizedViewName(authorizedViewName); + String authorizedViewId = extractAuthorizedViewIdFromAuthorizedViewName(authorizedViewName); + return AuthorizedViewId.of(tableId, authorizedViewId); + } + } } diff --git a/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/models/AuthorizedViewId.java b/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/models/AuthorizedViewId.java new file mode 100644 index 0000000000..5f64190b5c --- /dev/null +++ b/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/models/AuthorizedViewId.java @@ -0,0 +1,55 @@ +/* + * Copyright 2024 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.models; + +import com.google.api.core.InternalApi; +import com.google.auto.value.AutoValue; +import com.google.cloud.bigtable.data.v2.internal.NameUtil; +import com.google.common.base.Preconditions; + +/** + * An implementation of a {@link TargetId} for authorized views. + * + *

See {@link com.google.cloud.bigtable.admin.v2.models.AuthorizedView} for more details about an + * authorized view. + */ +@AutoValue +public abstract class AuthorizedViewId implements TargetId { + /** Constructs a new AuthorizedViewId object from the specified tableId and authorizedViewId. */ + public static AuthorizedViewId of(String tableId, String authorizedViewId) { + Preconditions.checkNotNull(tableId, "table id can't be null."); + Preconditions.checkNotNull(authorizedViewId, "authorized view id can't be null."); + return new AutoValue_AuthorizedViewId(tableId, authorizedViewId); + } + + abstract String getTableId(); + + abstract String getAuthorizedViewId(); + + @Override + @InternalApi + public String toResourceName(String projectId, String instanceId) { + return NameUtil.formatAuthorizedViewName( + projectId, instanceId, getTableId(), getAuthorizedViewId()); + } + + @Override + @InternalApi + public boolean scopedForAuthorizedView() { + return true; + } +} diff --git a/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/models/BulkMutation.java b/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/models/BulkMutation.java index a269370748..f6a09d0b6d 100644 --- a/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/models/BulkMutation.java +++ b/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/models/BulkMutation.java @@ -38,20 +38,31 @@ */ public final class BulkMutation implements Serializable, Cloneable { private static final long serialVersionUID = 3522061250439399088L; - - private final String tableId; + private final TargetId targetId; private transient MutateRowsRequest.Builder builder; private long mutationCountSum = 0; + /** @deprecated Please use {@link BulkMutation#create(TargetId)} instead. */ + @Deprecated public static BulkMutation create(String tableId) { - return new BulkMutation(tableId); + return new BulkMutation(TableId.of(tableId)); } - private BulkMutation(@Nonnull String tableId) { - Preconditions.checkNotNull(tableId); + /** + * Creates a new instance of the bulk mutation builder for the given target with targetId. + * + * @see AuthorizedViewId + * @see TableId + */ + public static BulkMutation create(TargetId targetId) { + return new BulkMutation(targetId); + } + + private BulkMutation(TargetId targetId) { + Preconditions.checkNotNull(targetId, "target id can't be null."); - this.tableId = tableId; + this.targetId = targetId; this.builder = MutateRowsRequest.newBuilder(); } @@ -117,14 +128,15 @@ public int getEntryCount() { @InternalApi public MutateRowsRequest toProto(RequestContext requestContext) { - String tableName = - NameUtil.formatTableName( - requestContext.getProjectId(), requestContext.getInstanceId(), tableId); - - return builder - .setTableName(tableName) - .setAppProfileId(requestContext.getAppProfileId()) - .build(); + String resourceName = + targetId.toResourceName(requestContext.getProjectId(), requestContext.getInstanceId()); + if (targetId.scopedForAuthorizedView()) { + builder.setAuthorizedViewName(resourceName); + } else { + builder.setTableName(resourceName); + } + + return builder.setAppProfileId(requestContext.getAppProfileId()).build(); } /** @@ -140,8 +152,11 @@ public MutateRowsRequest toProto(RequestContext requestContext) { */ @BetaApi public static BulkMutation fromProto(@Nonnull MutateRowsRequest request) { + String tableName = request.getTableName(); + String authorizedViewName = request.getAuthorizedViewName(); + BulkMutation bulkMutation = - BulkMutation.create(NameUtil.extractTableIdFromTableName(request.getTableName())); + BulkMutation.create(NameUtil.extractTargetId(tableName, authorizedViewName)); bulkMutation.builder = request.toBuilder(); return bulkMutation; @@ -150,7 +165,7 @@ public static BulkMutation fromProto(@Nonnull MutateRowsRequest request) { /** Creates a copy of {@link BulkMutation}. */ @Override public BulkMutation clone() { - BulkMutation bulkMutation = BulkMutation.create(tableId); + BulkMutation bulkMutation = BulkMutation.create(targetId); bulkMutation.builder = this.builder.clone(); return bulkMutation; } diff --git a/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/models/ConditionalRowMutation.java b/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/models/ConditionalRowMutation.java index ac4c548942..14841f9f4d 100644 --- a/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/models/ConditionalRowMutation.java +++ b/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/models/ConditionalRowMutation.java @@ -33,25 +33,49 @@ public final class ConditionalRowMutation implements Serializable { private static final long serialVersionUID = -3699904745621909502L; - private final String tableId; + private final TargetId targetId; private transient CheckAndMutateRowRequest.Builder builder = CheckAndMutateRowRequest.newBuilder(); - private ConditionalRowMutation(String tableId, ByteString rowKey) { - this.tableId = tableId; + private ConditionalRowMutation(TargetId targetId, ByteString rowKey) { + Preconditions.checkNotNull(targetId, "target id can't be null."); + + this.targetId = targetId; builder.setRowKey(rowKey); } - /** Creates a new instance of the mutation builder. */ + /** @deprecated Please use {@link ConditionalRowMutation#create(TargetId, String)} instead. */ + @Deprecated public static ConditionalRowMutation create(String tableId, String rowKey) { return create(tableId, ByteString.copyFromUtf8(rowKey)); } - /** Creates a new instance of the mutation builder. */ + /** + * Creates a new instance of the mutation builder for the given target with targetId. + * + * @see AuthorizedViewId + * @see TableId + */ + public static ConditionalRowMutation create(TargetId targetId, String rowKey) { + return create(targetId, ByteString.copyFromUtf8(rowKey)); + } + + /** @deprecated Please use {@link ConditionalRowMutation#create(TargetId, ByteString)} instead. */ + @Deprecated public static ConditionalRowMutation create(String tableId, ByteString rowKey) { Validations.validateTableId(tableId); - return new ConditionalRowMutation(tableId, rowKey); + return new ConditionalRowMutation(TableId.of(tableId), rowKey); + } + + /** + * Creates a new instance of the mutation builder for the given target with targetId. + * + * @see AuthorizedViewId + * @see TableId + */ + public static ConditionalRowMutation create(TargetId targetId, ByteString rowKey) { + return new ConditionalRowMutation(targetId, rowKey); } private void readObject(ObjectInputStream input) throws IOException, ClassNotFoundException { @@ -80,7 +104,8 @@ public ConditionalRowMutation condition(@Nonnull Filter condition) { Preconditions.checkNotNull(condition); Preconditions.checkState( !builder.hasPredicateFilter(), - "Can only have a single condition, please use a Filters#chain or Filters#interleave filter instead"); + "Can only have a single condition, please use a Filters#chain or Filters#interleave filter" + + " instead"); // TODO: verify that the condition does not use any FILTERS.condition() filters builder.setPredicateFilter(condition.toProto()); @@ -129,13 +154,16 @@ public CheckAndMutateRowRequest toProto(RequestContext requestContext) { Preconditions.checkState( !builder.getTrueMutationsList().isEmpty() || !builder.getFalseMutationsList().isEmpty(), "ConditionalRowMutations must have `then` or `otherwise` mutations."); - String tableName = - NameUtil.formatTableName( - requestContext.getProjectId(), requestContext.getInstanceId(), tableId); - return builder - .setTableName(tableName.toString()) - .setAppProfileId(requestContext.getAppProfileId()) - .build(); + + String resourceName = + targetId.toResourceName(requestContext.getProjectId(), requestContext.getInstanceId()); + if (targetId.scopedForAuthorizedView()) { + builder.setAuthorizedViewName(resourceName); + } else { + builder.setTableName(resourceName); + } + + return builder.setAppProfileId(requestContext.getAppProfileId()).build(); } /** @@ -146,9 +174,12 @@ public CheckAndMutateRowRequest toProto(RequestContext requestContext) { */ @BetaApi public static ConditionalRowMutation fromProto(@Nonnull CheckAndMutateRowRequest request) { - String tableId = NameUtil.extractTableIdFromTableName(request.getTableName()); + String tableName = request.getTableName(); + String authorizedViewName = request.getAuthorizedViewName(); + ConditionalRowMutation rowMutation = - ConditionalRowMutation.create(tableId, request.getRowKey()); + ConditionalRowMutation.create( + NameUtil.extractTargetId(tableName, authorizedViewName), request.getRowKey()); rowMutation.builder = request.toBuilder(); return rowMutation; diff --git a/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/models/MutateRowsException.java b/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/models/MutateRowsException.java index 4ae0606ab9..3eb6c6c7dd 100644 --- a/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/models/MutateRowsException.java +++ b/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/models/MutateRowsException.java @@ -33,20 +33,6 @@ * were part of that RPC. */ public final class MutateRowsException extends ApiException { - // Synthetic status to use for this ApiException subclass. - private static final StatusCode LOCAL_STATUS = - new StatusCode() { - @Override - public Code getCode() { - return Code.INTERNAL; - } - - @Override - public Object getTransportCode() { - return null; - } - }; - private final List failedMutations; /** @@ -56,6 +42,7 @@ public Object getTransportCode() { @InternalApi public static MutateRowsException create( @Nullable Throwable rpcError, + StatusCode status, @Nonnull List failedMutations, boolean retryable) { ErrorDetails errorDetails = null; @@ -63,15 +50,16 @@ public static MutateRowsException create( errorDetails = ((ApiException) rpcError).getErrorDetails(); } - return new MutateRowsException(rpcError, failedMutations, retryable, errorDetails); + return new MutateRowsException(rpcError, status, failedMutations, retryable, errorDetails); } private MutateRowsException( @Nullable Throwable rpcError, + StatusCode status, @Nonnull List failedMutations, boolean retryable, @Nullable ErrorDetails errorDetails) { - super(rpcError, LOCAL_STATUS, retryable, errorDetails); + super(rpcError, status, retryable, errorDetails); Preconditions.checkNotNull(failedMutations); Preconditions.checkArgument(!failedMutations.isEmpty(), "failedMutations can't be empty"); this.failedMutations = failedMutations; diff --git a/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/models/Query.java b/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/models/Query.java index c7e69d70d4..1b4cb8d680 100644 --- a/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/models/Query.java +++ b/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/models/Query.java @@ -47,20 +47,31 @@ public final class Query implements Serializable { // bigtable can server the largest filter size of 20KB. private static final int MAX_FILTER_SIZE = 20 * 1024; - private final String tableId; + private final TargetId targetId; private transient ReadRowsRequest.Builder builder = ReadRowsRequest.newBuilder(); + /** @deprecated Please use {@link Query#create(TargetId)} instead. */ + @Deprecated + public static Query create(String tableId) { + return new Query(TableId.of(tableId)); + } + /** - * Constructs a new Query object for the specified table id. The table id will be combined with - * the instance name specified in the {@link + * Constructs a new Query object for the given target with targetId. The target id will be + * combined with the instance name specified in the {@link * com.google.cloud.bigtable.data.v2.BigtableDataSettings}. + * + * @see AuthorizedViewId + * @see TableId */ - public static Query create(String tableId) { - return new Query(tableId); + public static Query create(TargetId targetId) { + return new Query(targetId); } - private Query(String tableId) { - this.tableId = tableId; + private Query(TargetId targetId) { + Preconditions.checkNotNull(targetId, "target id can't be null."); + + this.targetId = targetId; } private void readObject(ObjectInputStream input) throws IOException, ClassNotFoundException { @@ -260,7 +271,8 @@ public List shard(SortedSet splitPoints) { List shards = Lists.newArrayListWithCapacity(shardedRowSets.size()); for (RowSet rowSet : shardedRowSets) { - Query queryShard = new Query(tableId); + Query queryShard; + queryShard = new Query(targetId); queryShard.builder.mergeFrom(this.builder.build()); queryShard.builder.setRows(rowSet); shards.add(queryShard); @@ -303,14 +315,14 @@ public ByteStringRange getBound() { */ @InternalApi public ReadRowsRequest toProto(RequestContext requestContext) { - String tableName = - NameUtil.formatTableName( - requestContext.getProjectId(), requestContext.getInstanceId(), tableId); - - return builder - .setTableName(tableName) - .setAppProfileId(requestContext.getAppProfileId()) - .build(); + String resourceName = + targetId.toResourceName(requestContext.getProjectId(), requestContext.getInstanceId()); + if (targetId.scopedForAuthorizedView()) { + builder.setAuthorizedViewName(resourceName); + } else { + builder.setTableName(resourceName); + } + return builder.setAppProfileId(requestContext.getAppProfileId()).build(); } /** @@ -321,15 +333,17 @@ public ReadRowsRequest toProto(RequestContext requestContext) { */ public static Query fromProto(@Nonnull ReadRowsRequest request) { Preconditions.checkArgument(request != null, "ReadRowsRequest must not be null"); + String tableName = request.getTableName(); + String authorizedViewName = request.getAuthorizedViewName(); - Query query = new Query(NameUtil.extractTableIdFromTableName(request.getTableName())); + Query query = new Query(NameUtil.extractTargetId(tableName, authorizedViewName)); query.builder = request.toBuilder(); return query; } public Query clone() { - Query query = Query.create(tableId); + Query query = Query.create(targetId); query.builder = this.builder.clone(); return query; } @@ -424,7 +438,7 @@ public boolean equals(Object o) { return false; } Query query = (Query) o; - return Objects.equal(tableId, query.tableId) + return Objects.equal(targetId, query.targetId) && Objects.equal(builder.getRows(), query.builder.getRows()) && Objects.equal(builder.getFilter(), query.builder.getFilter()) && Objects.equal(builder.getRowsLimit(), query.builder.getRowsLimit()); @@ -433,7 +447,7 @@ public boolean equals(Object o) { @Override public int hashCode() { return Objects.hashCode( - tableId, builder.getRows(), builder.getFilter(), builder.getRowsLimit()); + targetId, builder.getRows(), builder.getFilter(), builder.getRowsLimit()); } @Override @@ -441,7 +455,7 @@ public String toString() { ReadRowsRequest request = builder.build(); return MoreObjects.toStringHelper(this) - .add("tableId", tableId) + .add("targetId", targetId) .add("keys", request.getRows().getRowKeysList()) .add("ranges", request.getRows().getRowRangesList()) .add("filter", request.getFilter()) diff --git a/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/models/ReadModifyWriteRow.java b/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/models/ReadModifyWriteRow.java index 5fa483d1bd..554a0268b9 100644 --- a/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/models/ReadModifyWriteRow.java +++ b/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/models/ReadModifyWriteRow.java @@ -33,25 +33,49 @@ public final class ReadModifyWriteRow implements Serializable { private static final long serialVersionUID = -8150045424541029193L; - private final String tableId; + private final TargetId targetId; private transient ReadModifyWriteRowRequest.Builder builder = ReadModifyWriteRowRequest.newBuilder(); - private ReadModifyWriteRow(@Nonnull String tableId, @Nonnull ByteString key) { - Preconditions.checkNotNull(tableId, "tableId can't be null."); + private ReadModifyWriteRow(TargetId targetId, ByteString key) { + Preconditions.checkNotNull(targetId, "target id can't be null."); Preconditions.checkNotNull(key, "key can't be null."); - this.tableId = tableId; + this.targetId = targetId; builder.setRowKey(key); } - public static ReadModifyWriteRow create(@Nonnull String tableId, @Nonnull String key) { + /** @deprecated Please use {@link ReadModifyWriteRow#create(TargetId, String)} instead. */ + @Deprecated + public static ReadModifyWriteRow create(String tableId, String key) { Preconditions.checkNotNull(key, "key can't be null."); - return new ReadModifyWriteRow(tableId, ByteString.copyFromUtf8(key)); + return new ReadModifyWriteRow(TableId.of(tableId), ByteString.copyFromUtf8(key)); } - public static ReadModifyWriteRow create(@Nonnull String tableId, @Nonnull ByteString key) { - return new ReadModifyWriteRow(tableId, key); + /** + * Creates a new instance of the ReadModifyWriteRow for the given target with targetId. + * + * @see AuthorizedViewId + * @see TableId + */ + public static ReadModifyWriteRow create(TargetId targetId, String key) { + return new ReadModifyWriteRow(targetId, ByteString.copyFromUtf8(key)); + } + + /** @deprecated Please use {@link ReadModifyWriteRow#create(TargetId, ByteString)} instead. */ + @Deprecated + public static ReadModifyWriteRow create(String tableId, ByteString key) { + return new ReadModifyWriteRow(TableId.of(tableId), key); + } + + /** + * Creates a new instance of the ReadModifyWriteRow for the given target with targetId. + * + * @see AuthorizedViewId + * @see TableId + */ + public static ReadModifyWriteRow create(TargetId targetId, ByteString key) { + return new ReadModifyWriteRow(targetId, key); } private void readObject(ObjectInputStream input) throws IOException, ClassNotFoundException { @@ -129,14 +153,14 @@ public ReadModifyWriteRow increment( @InternalApi public ReadModifyWriteRowRequest toProto(RequestContext requestContext) { - String tableName = - NameUtil.formatTableName( - requestContext.getProjectId(), requestContext.getInstanceId(), tableId); - - return builder - .setTableName(tableName) - .setAppProfileId(requestContext.getAppProfileId()) - .build(); + String resourceName = + targetId.toResourceName(requestContext.getProjectId(), requestContext.getInstanceId()); + if (targetId.scopedForAuthorizedView()) { + builder.setAuthorizedViewName(resourceName); + } else { + builder.setTableName(resourceName); + } + return builder.setAppProfileId(requestContext.getAppProfileId()).build(); } /** @@ -147,9 +171,12 @@ public ReadModifyWriteRowRequest toProto(RequestContext requestContext) { */ @BetaApi public static ReadModifyWriteRow fromProto(@Nonnull ReadModifyWriteRowRequest request) { - String tableId = NameUtil.extractTableIdFromTableName(request.getTableName()); + String tableName = request.getTableName(); + String authorizedViewName = request.getAuthorizedViewName(); - ReadModifyWriteRow row = ReadModifyWriteRow.create(tableId, request.getRowKey()); + ReadModifyWriteRow row = + ReadModifyWriteRow.create( + NameUtil.extractTargetId(tableName, authorizedViewName), request.getRowKey()); row.builder = request.toBuilder(); return row; diff --git a/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/models/RowMutation.java b/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/models/RowMutation.java index 940b76702c..4dfe751225 100644 --- a/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/models/RowMutation.java +++ b/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/models/RowMutation.java @@ -23,6 +23,7 @@ import com.google.cloud.bigtable.data.v2.internal.NameUtil; import com.google.cloud.bigtable.data.v2.internal.RequestContext; import com.google.cloud.bigtable.data.v2.models.Range.TimestampRange; +import com.google.common.base.Preconditions; import com.google.protobuf.ByteString; import java.io.Serializable; import javax.annotation.Nonnull; @@ -34,60 +35,102 @@ public final class RowMutation implements MutationApi, Serializable { private static final long serialVersionUID = 6529002234913236318L; - private final String tableId; + private final TargetId targetId; private final ByteString key; private final Mutation mutation; - private RowMutation(String tableId, ByteString key, Mutation mutation) { - this.tableId = tableId; + private RowMutation(TargetId targetId, ByteString key, Mutation mutation) { + Preconditions.checkNotNull(targetId, "target id can't be null."); + + this.targetId = targetId; this.key = key; this.mutation = mutation; } - /** Creates a new instance of the mutation builder. */ - public static RowMutation create(@Nonnull String tableId, @Nonnull String key) { + /** @deprecated Please use {@link RowMutation#create(TargetId, String)} instead. */ + @Deprecated + public static RowMutation create(String tableId, String key) { return create(tableId, ByteString.copyFromUtf8(key)); } - /** Creates a new instance of the mutation builder. */ - public static RowMutation create(@Nonnull String tableId, @Nonnull ByteString key) { - return new RowMutation(tableId, key, Mutation.create()); + /** + * Creates a new instance of the mutation builder for the given target with targetId. + * + * @see AuthorizedViewId + * @see TableId + */ + public static RowMutation create(TargetId targetId, String key) { + Preconditions.checkNotNull(targetId, "target id can't be null."); + return create(targetId, ByteString.copyFromUtf8(key)); + } + + /** @deprecated Please use {@link RowMutation#create(TargetId, ByteString)} instead. */ + @Deprecated + public static RowMutation create(String tableId, ByteString key) { + return new RowMutation(TableId.of(tableId), key, Mutation.create()); } /** - * Creates new instance of mutation builder by wrapping existing set of row mutations. The builder - * will be owned by this RowMutation and should not be used by the caller after this call. This - * functionality is intended for advanced usage. + * Creates a new instance of the mutation builder for the given target with targetId. + * + * @see AuthorizedViewId + * @see TableId + */ + public static RowMutation create(TargetId targetId, ByteString key) { + Preconditions.checkNotNull(targetId, "target id can't be null."); + return new RowMutation(targetId, key, Mutation.create()); + } + + /** @deprecated Please use {@link RowMutation#create(TargetId, String, Mutation)} instead. */ + @Deprecated + public static RowMutation create(String tableId, String key, Mutation mutation) { + return create(tableId, ByteString.copyFromUtf8(key), mutation); + } + + /** + * Creates new instance of mutation builder for the given target with targetId by wrapping + * existing set of row mutations. The builder will be owned by this RowMutation and should not be + * used by the caller after this call. This functionality is intended for advanced usage. * *

Sample code: * *


    * Mutation mutation = Mutation.create()
    *     .setCell("[FAMILY_NAME]", "[QUALIFIER]", [TIMESTAMP], "[VALUE]");
-   * RowMutation rowMutation = RowMutation.create("[TABLE]", "[ROW_KEY]", mutation);
+   * RowMutation rowMutation = RowMutation.create(TableId.of("[TABLE]"), "[ROW_KEY]", mutation);
    * 
+ * + * @see AuthorizedViewId + * @see TableId */ - public static RowMutation create( - @Nonnull String tableId, @Nonnull String key, @Nonnull Mutation mutation) { - return create(tableId, ByteString.copyFromUtf8(key), mutation); + public static RowMutation create(TargetId targetId, String key, Mutation mutation) { + return create(targetId, ByteString.copyFromUtf8(key), mutation); + } + + /** @deprecated Please use {@link RowMutation#create(TargetId, ByteString, Mutation)} instead. */ + @Deprecated + public static RowMutation create(String tableId, ByteString key, Mutation mutation) { + return new RowMutation(TableId.of(tableId), key, mutation); } /** - * Creates new instance of mutation builder by wrapping existing set of row mutations. The builder - * will be owned by this RowMutation and should not be used by the caller after this call. This - * functionality is intended for advanced usage. + * Creates new instance of mutation builder for the given target with targetId by wrapping + * existing set of row mutations. The builder will be owned by this RowMutation and should not be + * used by the caller after this call. This functionality is intended for advanced usage. * *

Sample code: * *


    * Mutation mutation = Mutation.create()
    *     .setCell("[FAMILY_NAME]", "[QUALIFIER]", [TIMESTAMP], "[VALUE]");
-   * RowMutation rowMutation = RowMutation.create("[TABLE]", [BYTE_STRING_ROW_KEY], mutation);
+   * RowMutation rowMutation = RowMutation.create(TableId.of("[TABLE]"), [BYTE_STRING_ROW_KEY], mutation);
    * 
+ * + * @see AuthorizedViewId + * @see TableId */ - public static RowMutation create( - @Nonnull String tableId, @Nonnull ByteString key, @Nonnull Mutation mutation) { - return new RowMutation(tableId, key, mutation); + public static RowMutation create(TargetId targetId, ByteString key, Mutation mutation) { + return new RowMutation(targetId, key, mutation); } @Override @@ -196,13 +239,17 @@ public RowMutation addToCell( @InternalApi public MutateRowRequest toProto(RequestContext requestContext) { - String tableName = - NameUtil.formatTableName( - requestContext.getProjectId(), requestContext.getInstanceId(), tableId); + MutateRowRequest.Builder builder = MutateRowRequest.newBuilder(); + String resourceName = + targetId.toResourceName(requestContext.getProjectId(), requestContext.getInstanceId()); + if (targetId.scopedForAuthorizedView()) { + builder.setAuthorizedViewName(resourceName); + } else { + builder.setTableName(resourceName); + } - return MutateRowRequest.newBuilder() + return builder .setAppProfileId(requestContext.getAppProfileId()) - .setTableName(tableName) .setRowKey(key) .addAllMutations(mutation.getMutations()) .build(); @@ -214,13 +261,17 @@ public MutateRowRequest toProto(RequestContext requestContext) { */ @InternalApi public MutateRowsRequest toBulkProto(RequestContext requestContext) { - String tableName = - NameUtil.formatTableName( - requestContext.getProjectId(), requestContext.getInstanceId(), tableId); + MutateRowsRequest.Builder builder = MutateRowsRequest.newBuilder(); + String resourceName = + targetId.toResourceName(requestContext.getProjectId(), requestContext.getInstanceId()); + if (targetId.scopedForAuthorizedView()) { + builder.setAuthorizedViewName(resourceName); + } else { + builder.setTableName(resourceName); + } - return MutateRowsRequest.newBuilder() + return builder .setAppProfileId(requestContext.getAppProfileId()) - .setTableName(tableName) .addEntries( Entry.newBuilder().setRowKey(key).addAllMutations(mutation.getMutations()).build()) .build(); @@ -239,9 +290,12 @@ public MutateRowsRequest toBulkProto(RequestContext requestContext) { */ @BetaApi public static RowMutation fromProto(@Nonnull MutateRowRequest request) { - String tableId = NameUtil.extractTableIdFromTableName(request.getTableName()); + String tableName = request.getTableName(); + String authorizedViewName = request.getAuthorizedViewName(); return RowMutation.create( - tableId, request.getRowKey(), Mutation.fromProto(request.getMutationsList())); + NameUtil.extractTargetId(tableName, authorizedViewName), + request.getRowKey(), + Mutation.fromProto(request.getMutationsList())); } } diff --git a/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/models/SampleRowKeysRequest.java b/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/models/SampleRowKeysRequest.java new file mode 100644 index 0000000000..08d9a3ca23 --- /dev/null +++ b/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/models/SampleRowKeysRequest.java @@ -0,0 +1,84 @@ +/* + * Copyright 2024 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.models; + +import com.google.api.core.InternalApi; +import com.google.cloud.bigtable.data.v2.internal.NameUtil; +import com.google.cloud.bigtable.data.v2.internal.RequestContext; +import com.google.common.base.Objects; +import com.google.common.base.Preconditions; +import java.io.Serializable; +import javax.annotation.Nonnull; + +/** Wraps a {@link com.google.bigtable.v2.SampleRowKeysRequest}. */ +public final class SampleRowKeysRequest implements Serializable { + private final TargetId targetId; + + private SampleRowKeysRequest(TargetId targetId) { + Preconditions.checkNotNull(targetId, "target id can't be null."); + this.targetId = targetId; + } + + /** Creates a new instance of the sample row keys builder for the given target with targetId */ + public static SampleRowKeysRequest create(TargetId targetId) { + return new SampleRowKeysRequest(targetId); + } + + @InternalApi + public com.google.bigtable.v2.SampleRowKeysRequest toProto(RequestContext requestContext) { + com.google.bigtable.v2.SampleRowKeysRequest.Builder builder = + com.google.bigtable.v2.SampleRowKeysRequest.newBuilder(); + String resourceName = + targetId.toResourceName(requestContext.getProjectId(), requestContext.getInstanceId()); + if (targetId.scopedForAuthorizedView()) { + builder.setAuthorizedViewName(resourceName); + } else { + builder.setTableName(resourceName); + } + return builder.setAppProfileId(requestContext.getAppProfileId()).build(); + } + + /** + * Wraps the protobuf {@link com.google.bigtable.v2.SampleRowKeysRequest}. + * + *

WARNING: Please note that the project id & instance id in the table/authorized view name + * will be overwritten by the configuration in the BigtableDataClient. + */ + @InternalApi + public static SampleRowKeysRequest fromProto( + @Nonnull com.google.bigtable.v2.SampleRowKeysRequest request) { + String tableName = request.getTableName(); + String authorizedViewName = request.getAuthorizedViewName(); + + SampleRowKeysRequest sampleRowKeysRequest = + SampleRowKeysRequest.create(NameUtil.extractTargetId(tableName, authorizedViewName)); + + return sampleRowKeysRequest; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + SampleRowKeysRequest sampleRowKeysRequest = (SampleRowKeysRequest) o; + return Objects.equal(targetId, sampleRowKeysRequest.targetId); + } +} diff --git a/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/models/TableId.java b/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/models/TableId.java new file mode 100644 index 0000000000..15b2cd9d95 --- /dev/null +++ b/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/models/TableId.java @@ -0,0 +1,47 @@ +/* + * Copyright 2024 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.models; + +import com.google.api.core.InternalApi; +import com.google.auto.value.AutoValue; +import com.google.cloud.bigtable.data.v2.internal.NameUtil; +import com.google.common.base.Preconditions; + +/** An implementation of a {@link TargetId} for tables. */ +@AutoValue +public abstract class TableId implements TargetId { + + /** Constructs a new TableId object for the specified table id. */ + public static TableId of(String tableId) { + Preconditions.checkNotNull(tableId, "table id can't be null."); + return new AutoValue_TableId(tableId); + } + + abstract String getTableId(); + + @Override + @InternalApi + public String toResourceName(String projectId, String instanceId) { + return NameUtil.formatTableName(projectId, instanceId, getTableId()); + } + + @Override + @InternalApi + public boolean scopedForAuthorizedView() { + return false; + } +} diff --git a/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/models/TargetId.java b/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/models/TargetId.java new file mode 100644 index 0000000000..ae5be23598 --- /dev/null +++ b/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/models/TargetId.java @@ -0,0 +1,47 @@ +/* + * Copyright 2024 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.models; + +import com.google.api.core.InternalApi; +import com.google.api.core.InternalExtensionOnly; +import java.io.Serializable; + +/** + * TargetId defines the scope a data operation can be applied to. + * + * @see AuthorizedViewId + * @see TableId + */ +@InternalExtensionOnly +public interface TargetId extends Serializable { + /** + * Combines the table or authorized view id with the projectId and instanceId to form the actual + * resource name in the request protobuf. + * + *

This method is considered an internal implementation detail and not meant to be used by + * applications. + */ + @InternalApi + String toResourceName(String projectId, String instanceId); + + /** + * Returns true if this TargetId object represents id for an authorized view (rather than a + * table). + */ + @InternalApi + boolean scopedForAuthorizedView(); +} diff --git a/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/stub/BigtableBatchingCallSettings.java b/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/stub/BigtableBatchingCallSettings.java index 2ca5e10211..3e2b540635 100644 --- a/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/stub/BigtableBatchingCallSettings.java +++ b/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/stub/BigtableBatchingCallSettings.java @@ -28,6 +28,7 @@ import com.google.api.gax.rpc.UnaryCallSettings; import com.google.cloud.bigtable.data.v2.models.BulkMutation; import com.google.cloud.bigtable.data.v2.models.RowMutationEntry; +import com.google.cloud.bigtable.data.v2.stub.mutaterows.MutateRowsAttemptResult; import com.google.common.base.MoreObjects; import com.google.common.base.Preconditions; import java.util.Set; @@ -57,11 +58,12 @@ * @see RetrySettings for retry configuration. */ @BetaApi("This surface is likely to change as the batching surface evolves.") -public final class BigtableBatchingCallSettings extends UnaryCallSettings { +public final class BigtableBatchingCallSettings + extends UnaryCallSettings { // This settings is just a simple wrapper for BatchingCallSettings to allow us to add // additional functionality. - private final BatchingCallSettings + private final BatchingCallSettings batchingCallSettings; private final boolean isLatencyBasedThrottlingEnabled; private final Long targetRpcLatencyMs; @@ -89,7 +91,8 @@ public BatchingSettings getBatchingSettings() { } /** Returns an adapter that packs and unpacks batching elements. */ - BatchingDescriptor getBatchingDescriptor() { + BatchingDescriptor + getBatchingDescriptor() { return batchingCallSettings.getBatchingDescriptor(); } @@ -120,7 +123,8 @@ public boolean isServerInitiatedFlowControlEnabled() { } static Builder newBuilder( - BatchingDescriptor batchingDescriptor) { + BatchingDescriptor + batchingDescriptor) { return new Builder(batchingDescriptor); } @@ -148,9 +152,11 @@ public String toString() { * A base builder class for {@link BigtableBatchingCallSettings}. See the class documentation of * {@link BigtableBatchingCallSettings} for a description of the different values that can be set. */ - public static class Builder extends UnaryCallSettings.Builder { + public static class Builder + extends UnaryCallSettings.Builder { - private BatchingDescriptor batchingDescriptor; + private BatchingDescriptor + batchingDescriptor; private BatchingSettings batchingSettings; private boolean isLatencyBasedThrottlingEnabled; private Long targetRpcLatencyMs; @@ -160,7 +166,8 @@ public static class Builder extends UnaryCallSettings.Builder batchingDescriptor) { + BatchingDescriptor + batchingDescriptor) { this.batchingDescriptor = Preconditions.checkNotNull(batchingDescriptor, "batching descriptor can't be null"); } 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 a65c0ada92..ec15c4131a 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 @@ -66,10 +66,10 @@ import com.google.bigtable.v2.ReadRowsRequest; import com.google.bigtable.v2.ReadRowsResponse; import com.google.bigtable.v2.RowRange; -import com.google.bigtable.v2.SampleRowKeysRequest; import com.google.bigtable.v2.SampleRowKeysResponse; import com.google.cloud.bigtable.Version; import com.google.cloud.bigtable.data.v2.internal.JwtCredentialsWithAudience; +import com.google.cloud.bigtable.data.v2.internal.NameUtil; import com.google.cloud.bigtable.data.v2.internal.RequestContext; import com.google.cloud.bigtable.data.v2.models.BulkMutation; import com.google.cloud.bigtable.data.v2.models.ChangeStreamMutation; @@ -87,6 +87,8 @@ import com.google.cloud.bigtable.data.v2.models.RowAdapter; import com.google.cloud.bigtable.data.v2.models.RowMutation; import com.google.cloud.bigtable.data.v2.models.RowMutationEntry; +import com.google.cloud.bigtable.data.v2.models.SampleRowKeysRequest; +import com.google.cloud.bigtable.data.v2.models.TargetId; import com.google.cloud.bigtable.data.v2.stub.changestream.ChangeStreamRecordMergingCallable; import com.google.cloud.bigtable.data.v2.stub.changestream.GenerateInitialChangeStreamPartitionsUserCallable; import com.google.cloud.bigtable.data.v2.stub.changestream.ReadChangeStreamResumptionStrategy; @@ -102,7 +104,9 @@ import com.google.cloud.bigtable.data.v2.stub.metrics.StatsHeadersUnaryCallable; import com.google.cloud.bigtable.data.v2.stub.metrics.TracedBatcherUnaryCallable; import com.google.cloud.bigtable.data.v2.stub.mutaterows.BulkMutateRowsUserFacingCallable; +import com.google.cloud.bigtable.data.v2.stub.mutaterows.MutateRowsAttemptResult; import com.google.cloud.bigtable.data.v2.stub.mutaterows.MutateRowsBatchingDescriptor; +import com.google.cloud.bigtable.data.v2.stub.mutaterows.MutateRowsPartialErrorRetryAlgorithm; import com.google.cloud.bigtable.data.v2.stub.mutaterows.MutateRowsRetryingCallable; import com.google.cloud.bigtable.data.v2.stub.readrows.FilterMarkerRowsCallable; import com.google.cloud.bigtable.data.v2.stub.readrows.ReadRowsBatchingDescriptor; @@ -164,8 +168,11 @@ public class EnhancedBigtableStub implements AutoCloseable { private final UnaryCallable readRowCallable; private final UnaryCallable> bulkReadRowsCallable; private final UnaryCallable> sampleRowKeysCallable; + private final UnaryCallable> + sampleRowKeysCallableWithRequest; private final UnaryCallable mutateRowCallable; - private final UnaryCallable bulkMutateRowsCallable; + private final UnaryCallable bulkMutateRowsCallable; + private final UnaryCallable externalBulkMutateRowsCallable; private final UnaryCallable checkAndMutateRowCallable; private final UnaryCallable readModifyWriteRowCallable; private final UnaryCallable pingAndWarmCallable; @@ -367,8 +374,11 @@ public EnhancedBigtableStub( readRowCallable = createReadRowCallable(new DefaultRowAdapter()); bulkReadRowsCallable = createBulkReadRowsCallable(new DefaultRowAdapter()); sampleRowKeysCallable = createSampleRowKeysCallable(); + sampleRowKeysCallableWithRequest = createSampleRowKeysCallableWithRequest(); mutateRowCallable = createMutateRowCallable(); - bulkMutateRowsCallable = createBulkMutateRowsCallable(); + bulkMutateRowsCallable = createMutateRowsBaseCallable(); + externalBulkMutateRowsCallable = + new MutateRowsErrorConverterUnaryCallable(bulkMutateRowsCallable); checkAndMutateRowCallable = createCheckAndMutateRowCallable(); readModifyWriteRowCallable = createReadModifyWriteRowCallable(); generateInitialChangeStreamPartitionsCallable = @@ -493,9 +503,17 @@ private ServerStreamingCallable createReadRo new RequestParamsExtractor() { @Override public Map extract(ReadRowsRequest readRowsRequest) { + String tableName = readRowsRequest.getTableName(); + String authorizedViewName = readRowsRequest.getAuthorizedViewName(); + if (tableName.isEmpty()) { + tableName = + NameUtil.extractTableNameFromAuthorizedViewName(authorizedViewName); + } return ImmutableMap.of( - "table_name", readRowsRequest.getTableName(), - "app_profile_id", readRowsRequest.getAppProfileId()); + "table_name", + tableName, + "app_profile_id", + readRowsRequest.getAppProfileId()); } }) .build(), @@ -578,6 +596,57 @@ private UnaryCallable> createBulkReadRowsCallable( return traced.withDefaultCallContext(clientContext.getDefaultCallContext()); } + /** + * Helper function that should only be used by createSampleRowKeysCallable() and + * createSampleRowKeysWithRequestCallable(). + */ + private UnaryCallable> + createSampleRowKeysBaseCallable() { + ServerStreamingCallable + base = + GrpcRawCallableFactory.createServerStreamingCallable( + GrpcCallSettings + . + newBuilder() + .setMethodDescriptor(BigtableGrpc.getSampleRowKeysMethod()) + .setParamsExtractor( + new RequestParamsExtractor() { + @Override + public Map extract( + com.google.bigtable.v2.SampleRowKeysRequest sampleRowKeysRequest) { + String tableName = sampleRowKeysRequest.getTableName(); + String authorizedViewName = + sampleRowKeysRequest.getAuthorizedViewName(); + if (tableName.isEmpty()) { + tableName = + NameUtil.extractTableNameFromAuthorizedViewName( + authorizedViewName); + } + return ImmutableMap.of( + "table_name", + tableName, + "app_profile_id", + sampleRowKeysRequest.getAppProfileId()); + } + }) + .build(), + settings.sampleRowKeysSettings().getRetryableCodes()); + + UnaryCallable> + spoolable = base.all(); + + UnaryCallable> + withStatsHeaders = new StatsHeadersUnaryCallable<>(spoolable); + + UnaryCallable> + withBigtableTracer = new BigtableTracerUnaryCallable<>(withStatsHeaders); + + UnaryCallable> + retryable = withRetries(withBigtableTracer, settings.sampleRowKeysSettings()); + + return retryable; + } + /** * Creates a callable chain to handle SampleRowKeys RPcs. The chain will: * @@ -593,36 +662,33 @@ private UnaryCallable> createBulkReadRowsCallable( private UnaryCallable> createSampleRowKeysCallable() { String methodName = "SampleRowKeys"; - ServerStreamingCallable base = - GrpcRawCallableFactory.createServerStreamingCallable( - GrpcCallSettings.newBuilder() - .setMethodDescriptor(BigtableGrpc.getSampleRowKeysMethod()) - .setParamsExtractor( - new RequestParamsExtractor() { - @Override - public Map extract( - SampleRowKeysRequest sampleRowKeysRequest) { - return ImmutableMap.of( - "table_name", sampleRowKeysRequest.getTableName(), - "app_profile_id", sampleRowKeysRequest.getAppProfileId()); - } - }) - .build(), - settings.sampleRowKeysSettings().getRetryableCodes()); - - UnaryCallable> spoolable = base.all(); - - UnaryCallable> withStatsHeaders = - new StatsHeadersUnaryCallable<>(spoolable); - - UnaryCallable> withBigtableTracer = - new BigtableTracerUnaryCallable<>(withStatsHeaders); + UnaryCallable> + baseCallable = createSampleRowKeysBaseCallable(); + return createUserFacingUnaryCallable( + methodName, new SampleRowKeysCallable(baseCallable, requestContext)); + } - UnaryCallable> retryable = - withRetries(withBigtableTracer, settings.sampleRowKeysSettings()); + /** + * Creates a callable chain to handle SampleRowKeys RPcs. The chain will: + * + *

    + *
  • Convert a {@link SampleRowKeysRequest} to a {@link + * com.google.bigtable.v2.SampleRowKeysRequest}. + *
  • Dispatch the request to the GAPIC's {@link BigtableStub#sampleRowKeysCallable()}. + *
  • Spool responses into a list. + *
  • Retry on failure. + *
  • Convert the responses into {@link KeyOffset}s. + *
  • Add tracing & metrics. + *
+ */ + private UnaryCallable> + createSampleRowKeysCallableWithRequest() { + String methodName = "SampleRowKeys"; + UnaryCallable> + baseCallable = createSampleRowKeysBaseCallable(); return createUserFacingUnaryCallable( - methodName, new SampleRowKeysCallable(retryable, requestContext)); + methodName, new SampleRowKeysCallableWithRequest(baseCallable, requestContext)); } /** @@ -643,9 +709,17 @@ private UnaryCallable createMutateRowCallable() { new RequestParamsExtractor() { @Override public Map extract(MutateRowRequest mutateRowRequest) { + String tableName = mutateRowRequest.getTableName(); + String authorizedViewName = mutateRowRequest.getAuthorizedViewName(); + if (tableName.isEmpty()) { + tableName = + NameUtil.extractTableNameFromAuthorizedViewName(authorizedViewName); + } return ImmutableMap.of( - "table_name", mutateRowRequest.getTableName(), - "app_profile_id", mutateRowRequest.getAppProfileId()); + "table_name", + tableName, + "app_profile_id", + mutateRowRequest.getAppProfileId()); } }) .build(), @@ -665,14 +739,24 @@ public Map extract(MutateRowRequest mutateRowRequest) { } /** - * Internal helper to create the base MutateRows callable chain. The chain is responsible for - * retrying individual entry in case of error. + * Creates a callable chain to handle MutatesRows RPCs. This is meant to be used for manual + * batching. The chain will: * - *

NOTE: the caller is responsible for adding tracing & metrics. + *

    + *
  • Convert a {@link BulkMutation} into a {@link MutateRowsRequest}. + *
  • Process the response and schedule retries. At the end of each attempt, entries that have + * been applied, are filtered from the next attempt. Also, any entries that failed with a + * nontransient error, are filtered from the next attempt. This will continue until there + * are no more entries or there are no more retry attempts left. + *
  • Wrap batch failures in a {@link MutateRowsAttemptResult}. + *
  • Add tracing & metrics. + *
* - * @see MutateRowsRetryingCallable for more details + * This callable returns an internal type {@link MutateRowsAttemptResult}. + * + *

This function should not be exposed to external users, as it could cause a data loss. */ - private UnaryCallable createMutateRowsBaseCallable() { + private UnaryCallable createMutateRowsBaseCallable() { ServerStreamingCallable base = GrpcRawCallableFactory.createServerStreamingCallable( GrpcCallSettings.newBuilder() @@ -681,9 +765,17 @@ private UnaryCallable createMutateRowsBaseCallable() { new RequestParamsExtractor() { @Override public Map extract(MutateRowsRequest mutateRowsRequest) { + String tableName = mutateRowsRequest.getTableName(); + String authorizedViewName = mutateRowsRequest.getAuthorizedViewName(); + if (tableName.isEmpty()) { + tableName = + NameUtil.extractTableNameFromAuthorizedViewName(authorizedViewName); + } return ImmutableMap.of( - "table_name", mutateRowsRequest.getTableName(), - "app_profile_id", mutateRowsRequest.getAppProfileId()); + "table_name", + tableName, + "app_profile_id", + mutateRowsRequest.getAppProfileId()); } }) .build(), @@ -706,55 +798,38 @@ public Map extract(MutateRowsRequest mutateRowsRequest) { ServerStreamingCallable withBigtableTracer = new BigtableTracerStreamingCallable<>(convertException); - BasicResultRetryAlgorithm resultRetryAlgorithm; + BasicResultRetryAlgorithm resultRetryAlgorithm; if (settings.getEnableRetryInfo()) { resultRetryAlgorithm = new RetryInfoRetryAlgorithm<>(); } else { resultRetryAlgorithm = new ApiResultRetryAlgorithm<>(); } + MutateRowsPartialErrorRetryAlgorithm mutateRowsPartialErrorRetryAlgorithm = + new MutateRowsPartialErrorRetryAlgorithm(resultRetryAlgorithm); - RetryAlgorithm retryAlgorithm = + RetryAlgorithm retryAlgorithm = new RetryAlgorithm<>( - resultRetryAlgorithm, + mutateRowsPartialErrorRetryAlgorithm, new ExponentialRetryAlgorithm( settings.bulkMutateRowsSettings().getRetrySettings(), clientContext.getClock())); - RetryingExecutorWithContext retryingExecutor = + RetryingExecutorWithContext retryingExecutor = new ScheduledRetryingExecutor<>(retryAlgorithm, clientContext.getExecutor()); + UnaryCallable baseCallable = + new MutateRowsRetryingCallable( + clientContext.getDefaultCallContext(), + withBigtableTracer, + retryingExecutor, + settings.bulkMutateRowsSettings().getRetryableCodes(), + retryAlgorithm); - return new MutateRowsRetryingCallable( - clientContext.getDefaultCallContext(), - withBigtableTracer, - retryingExecutor, - settings.bulkMutateRowsSettings().getRetryableCodes(), - retryAlgorithm); - } - - /** - * Creates a callable chain to handle MutatesRows RPCs. This is meant to be used for manual - * batching. The chain will: - * - *

    - *
  • Convert a {@link BulkMutation} into a {@link MutateRowsRequest}. - *
  • Process the response and schedule retries. At the end of each attempt, entries that have - * been applied, are filtered from the next attempt. Also, any entries that failed with a - * nontransient error, are filtered from the next attempt. This will continue until there - * are no more entries or there are no more retry attempts left. - *
  • Wrap batch failures in a {@link - * com.google.cloud.bigtable.data.v2.models.MutateRowsException}. - *
  • Add tracing & metrics. - *
- */ - private UnaryCallable createBulkMutateRowsCallable() { - UnaryCallable baseCallable = createMutateRowsBaseCallable(); - - UnaryCallable withCookie = baseCallable; + UnaryCallable withCookie = baseCallable; if (settings.getEnableRoutingCookie()) { withCookie = new CookiesUnaryCallable<>(baseCallable); } - UnaryCallable flowControlCallable = null; + UnaryCallable flowControlCallable = null; if (settings.bulkMutateRowsSettings().isLatencyBasedThrottlingEnabled()) { flowControlCallable = new DynamicFlowControlCallable( @@ -764,16 +839,16 @@ private UnaryCallable createBulkMutateRowsCallable() { settings.bulkMutateRowsSettings().getTargetRpcLatencyMs(), FLOW_CONTROL_ADJUSTING_INTERVAL_MS); } - UnaryCallable userFacing = + UnaryCallable userFacing = new BulkMutateRowsUserFacingCallable( flowControlCallable != null ? flowControlCallable : withCookie, requestContext); SpanName spanName = getSpanName("MutateRows"); - UnaryCallable tracedBatcherUnaryCallable = + UnaryCallable tracedBatcherUnaryCallable = new TracedBatcherUnaryCallable<>(userFacing); - UnaryCallable traced = + UnaryCallable traced = new TracedUnaryCallable<>( tracedBatcherUnaryCallable, clientContext.getTracerFactory(), spanName); @@ -811,6 +886,37 @@ public Batcher newMutateRowsBatcher( MoreObjects.firstNonNull(ctx, clientContext.getDefaultCallContext())); } + /** + * Creates a {@link BatcherImpl} to handle {@link MutateRowsRequest.Entry} mutations. This is + * meant to be used for automatic batching with flow control. + * + *
    + *
  • Uses {@link MutateRowsBatchingDescriptor} to spool the {@link RowMutationEntry} mutations + * and send them out as {@link BulkMutation}. + *
  • Uses {@link #bulkMutateRowsCallable()} to perform RPC. + *
  • Batching thresholds can be configured from {@link + * EnhancedBigtableStubSettings#bulkMutateRowsSettings()}. + *
  • Process the response and schedule retries. At the end of each attempt, entries that have + * been applied, are filtered from the next attempt. Also, any entries that failed with a + * nontransient error, are filtered from the next attempt. This will continue until there + * are no more entries or there are no more retry attempts left. + *
  • Wrap batch failures in a {@link + * com.google.cloud.bigtable.data.v2.models.MutateRowsException}. + *
  • Split the responses using {@link MutateRowsBatchingDescriptor}. + *
+ */ + public Batcher newMutateRowsBatcher( + TargetId targetId, @Nullable GrpcCallContext ctx) { + return new BatcherImpl<>( + settings.bulkMutateRowsSettings().getBatchingDescriptor(), + bulkMutateRowsCallable, + BulkMutation.create(targetId), + settings.bulkMutateRowsSettings().getBatchingSettings(), + clientContext.getExecutor(), + bulkMutationFlowController, + MoreObjects.firstNonNull(ctx, clientContext.getDefaultCallContext())); + } + /** * Creates a {@link BatcherImpl} to handle {@link Query#rowKey(String)}. This is meant for bulk * read with flow control. @@ -859,9 +965,18 @@ private UnaryCallable createCheckAndMutateRowCa @Override public Map extract( CheckAndMutateRowRequest checkAndMutateRowRequest) { + String tableName = checkAndMutateRowRequest.getTableName(); + String authorizedViewName = + checkAndMutateRowRequest.getAuthorizedViewName(); + if (tableName.isEmpty()) { + tableName = + NameUtil.extractTableNameFromAuthorizedViewName(authorizedViewName); + } return ImmutableMap.of( - "table_name", checkAndMutateRowRequest.getTableName(), - "app_profile_id", checkAndMutateRowRequest.getAppProfileId()); + "table_name", + tableName, + "app_profile_id", + checkAndMutateRowRequest.getAppProfileId()); } }) .build(), @@ -899,9 +1014,14 @@ private UnaryCallable createReadModifyWriteRowCallable( new RequestParamsExtractor() { @Override public Map extract(ReadModifyWriteRowRequest request) { + String tableName = request.getTableName(); + String authorizedViewName = request.getAuthorizedViewName(); + if (tableName.isEmpty()) { + tableName = + NameUtil.extractTableNameFromAuthorizedViewName(authorizedViewName); + } return ImmutableMap.of( - "table_name", request.getTableName(), - "app_profile_id", request.getAppProfileId()); + "table_name", tableName, "app_profile_id", request.getAppProfileId()); } }) .build(), @@ -1149,6 +1269,7 @@ private ServerStreamingCallable withR } return retrying; } + // // @@ -1166,15 +1287,24 @@ public UnaryCallable> sampleRowKeysCallable() { return sampleRowKeysCallable; } + public UnaryCallable> sampleRowKeysCallableWithRequest() { + return sampleRowKeysCallableWithRequest; + } + public UnaryCallable mutateRowCallable() { return mutateRowCallable; } /** - * Returns the callable chain created in {@link #createBulkMutateRowsCallable()} ()} during stub + * Returns the callable chain created in {@link #createMutateRowsBaseCallable()} during stub * construction. */ public UnaryCallable bulkMutateRowsCallable() { + return externalBulkMutateRowsCallable; + } + + @InternalApi + public UnaryCallable internalBulkMutateRowsCallable() { return bulkMutateRowsCallable; } @@ -1209,6 +1339,7 @@ public UnaryCallable readModifyWriteRowCallable() { UnaryCallable pingAndWarmCallable() { return pingAndWarmCallable; } + // private SpanName getSpanName(String methodName) { diff --git a/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/stub/MutateRowsErrorConverterUnaryCallable.java b/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/stub/MutateRowsErrorConverterUnaryCallable.java new file mode 100644 index 0000000000..2b118df61e --- /dev/null +++ b/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/stub/MutateRowsErrorConverterUnaryCallable.java @@ -0,0 +1,61 @@ +/* + * Copyright 2024 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.stub; + +import com.google.api.core.ApiFuture; +import com.google.api.core.ApiFutures; +import com.google.api.core.InternalApi; +import com.google.api.gax.grpc.GrpcStatusCode; +import com.google.api.gax.rpc.ApiCallContext; +import com.google.api.gax.rpc.UnaryCallable; +import com.google.cloud.bigtable.data.v2.models.BulkMutation; +import com.google.cloud.bigtable.data.v2.models.MutateRowsException; +import com.google.cloud.bigtable.data.v2.stub.mutaterows.MutateRowsAttemptResult; +import com.google.common.util.concurrent.MoreExecutors; +import io.grpc.Status; + +/** + * This callable converts partial batch failures into an exception. This is necessary to make sure + * that the caller properly handles issues and avoids possible data loss on partial failures + */ +@InternalApi +public class MutateRowsErrorConverterUnaryCallable extends UnaryCallable { + + private final UnaryCallable innerCallable; + + public MutateRowsErrorConverterUnaryCallable( + UnaryCallable callable) { + this.innerCallable = callable; + } + + @Override + public ApiFuture futureCall(BulkMutation request, ApiCallContext context) { + ApiFuture future = innerCallable.futureCall(request, context); + return ApiFutures.transform( + future, + result -> { + if (!result.getFailedMutations().isEmpty()) { + throw MutateRowsException.create( + null, + GrpcStatusCode.of(Status.Code.OK), + result.getFailedMutations(), + result.getIsRetryable()); + } + return null; + }, + MoreExecutors.directExecutor()); + } +} diff --git a/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/stub/SampleRowKeysCallableWithRequest.java b/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/stub/SampleRowKeysCallableWithRequest.java new file mode 100644 index 0000000000..034a4048d0 --- /dev/null +++ b/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/stub/SampleRowKeysCallableWithRequest.java @@ -0,0 +1,74 @@ +/* + * Copyright 2024 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.stub; + +import com.google.api.core.ApiFunction; +import com.google.api.core.ApiFuture; +import com.google.api.core.ApiFutures; +import com.google.api.gax.rpc.ApiCallContext; +import com.google.api.gax.rpc.UnaryCallable; +import com.google.bigtable.v2.SampleRowKeysResponse; +import com.google.cloud.bigtable.data.v2.internal.RequestContext; +import com.google.cloud.bigtable.data.v2.models.KeyOffset; +import com.google.cloud.bigtable.data.v2.models.SampleRowKeysRequest; +import com.google.common.collect.ImmutableList; +import com.google.common.util.concurrent.MoreExecutors; +import java.util.List; + +/** Simple wrapper for SampleRowKeys to wrap the request and response protobufs. */ +class SampleRowKeysCallableWithRequest + extends UnaryCallable> { + private final RequestContext requestContext; + private final UnaryCallable< + com.google.bigtable.v2.SampleRowKeysRequest, List> + inner; + + SampleRowKeysCallableWithRequest( + UnaryCallable> inner, + RequestContext requestContext) { + + this.requestContext = requestContext; + this.inner = inner; + } + + @Override + public ApiFuture> futureCall( + SampleRowKeysRequest request, ApiCallContext context) { + ApiFuture> rawResponse = + inner.futureCall(request.toProto(requestContext), context); + + return ApiFutures.transform( + rawResponse, + new ApiFunction, List>() { + @Override + public List apply(List rawResponse) { + return convert(rawResponse); + } + }, + MoreExecutors.directExecutor()); + } + + private static List convert(List rawResponse) { + ImmutableList.Builder results = ImmutableList.builder(); + + for (SampleRowKeysResponse element : rawResponse) { + results.add(KeyOffset.create(element.getRowKey(), element.getOffsetBytes())); + } + + return results.build(); + } +} diff --git a/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/stub/metrics/Util.java b/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/stub/metrics/Util.java index 8baf6a15f4..4c3fd7a42d 100644 --- a/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/stub/metrics/Util.java +++ b/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/stub/metrics/Util.java @@ -22,6 +22,7 @@ import com.google.api.gax.rpc.ApiException; import com.google.api.gax.rpc.StatusCode; import com.google.api.gax.rpc.StatusCode.Code; +import com.google.bigtable.v2.AuthorizedViewName; import com.google.bigtable.v2.CheckAndMutateRowRequest; import com.google.bigtable.v2.MutateRowRequest; import com.google.bigtable.v2.MutateRowsRequest; @@ -30,7 +31,6 @@ import com.google.bigtable.v2.ResponseParams; import com.google.bigtable.v2.SampleRowKeysRequest; import com.google.bigtable.v2.TableName; -import com.google.common.base.Strings; import com.google.common.collect.ImmutableMap; import com.google.protobuf.InvalidProtocolBufferException; import io.grpc.CallOptions; @@ -108,20 +108,33 @@ static TagValue extractStatusFromFuture(Future future) { static String extractTableId(Object request) { String tableName = null; + String authorizedViewName = null; if (request instanceof ReadRowsRequest) { tableName = ((ReadRowsRequest) request).getTableName(); + authorizedViewName = ((ReadRowsRequest) request).getAuthorizedViewName(); } else if (request instanceof MutateRowsRequest) { tableName = ((MutateRowsRequest) request).getTableName(); + authorizedViewName = ((MutateRowsRequest) request).getAuthorizedViewName(); } else if (request instanceof MutateRowRequest) { tableName = ((MutateRowRequest) request).getTableName(); + authorizedViewName = ((MutateRowRequest) request).getAuthorizedViewName(); } else if (request instanceof SampleRowKeysRequest) { tableName = ((SampleRowKeysRequest) request).getTableName(); + authorizedViewName = ((SampleRowKeysRequest) request).getAuthorizedViewName(); } else if (request instanceof CheckAndMutateRowRequest) { tableName = ((CheckAndMutateRowRequest) request).getTableName(); + authorizedViewName = ((CheckAndMutateRowRequest) request).getAuthorizedViewName(); } else if (request instanceof ReadModifyWriteRowRequest) { tableName = ((ReadModifyWriteRowRequest) request).getTableName(); + authorizedViewName = ((ReadModifyWriteRowRequest) request).getAuthorizedViewName(); + } + if (tableName == null && authorizedViewName == null) return "undefined"; + if (tableName.isEmpty() && authorizedViewName.isEmpty()) return "undefined"; + if (!tableName.isEmpty()) { + return TableName.parse(tableName).getTable(); + } else { + return AuthorizedViewName.parse(authorizedViewName).getTable(); } - return !Strings.isNullOrEmpty(tableName) ? TableName.parse(tableName).getTable() : "undefined"; } /** diff --git a/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/stub/mutaterows/BulkMutateRowsUserFacingCallable.java b/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/stub/mutaterows/BulkMutateRowsUserFacingCallable.java index 8048cceaad..94980a80a2 100644 --- a/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/stub/mutaterows/BulkMutateRowsUserFacingCallable.java +++ b/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/stub/mutaterows/BulkMutateRowsUserFacingCallable.java @@ -30,18 +30,22 @@ * applications. */ @InternalApi -public final class BulkMutateRowsUserFacingCallable extends UnaryCallable { - private final UnaryCallable innerCallable; +public final class BulkMutateRowsUserFacingCallable + extends UnaryCallable { + + private final UnaryCallable innerCallable; private final RequestContext requestContext; public BulkMutateRowsUserFacingCallable( - UnaryCallable innerCallable, RequestContext requestContext) { + UnaryCallable innerCallable, + RequestContext requestContext) { this.innerCallable = innerCallable; this.requestContext = requestContext; } @Override - public ApiFuture futureCall(BulkMutation request, ApiCallContext context) { + public ApiFuture futureCall( + BulkMutation request, ApiCallContext context) { return innerCallable.futureCall(request.toProto(requestContext), context); } } diff --git a/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/stub/mutaterows/MutateRowsAttemptCallable.java b/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/stub/mutaterows/MutateRowsAttemptCallable.java index 155ea43211..b07e67ba94 100644 --- a/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/stub/mutaterows/MutateRowsAttemptCallable.java +++ b/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/stub/mutaterows/MutateRowsAttemptCallable.java @@ -87,7 +87,7 @@ * *

Package-private for internal use. */ -class MutateRowsAttemptCallable implements Callable { +class MutateRowsAttemptCallable implements Callable { // Synthetic status for Mutations that didn't get a result (because the whole RPC failed). It will // be exposed in MutateRowsException's FailedMutations. private static final StatusCode LOCAL_UNKNOWN_STATUS = @@ -116,17 +116,17 @@ public Object getTransportCode() { @Nonnull private TimedAttemptSettings attemptSettings; // Parent controller - private RetryingFuture externalFuture; + private RetryingFuture externalFuture; // Simple wrappers for handling result futures - private final ApiFunction, Void> attemptSuccessfulCallback = - new ApiFunction, Void>() { - @Override - public Void apply(List responses) { - handleAttemptSuccess(responses); - return null; - } - }; + private final ApiFunction, MutateRowsAttemptResult> + attemptSuccessfulCallback = + new ApiFunction, MutateRowsAttemptResult>() { + @Override + public MutateRowsAttemptResult apply(List responses) { + return handleAttemptSuccess(responses); + } + }; private final ApiFunction> attemptFailedCallback = new ApiFunction>() { @@ -153,7 +153,7 @@ public List apply(Throwable throwable) { permanentFailures = Lists.newArrayList(); } - public void setExternalFuture(RetryingFuture externalFuture) { + public void setExternalFuture(RetryingFuture externalFuture) { this.externalFuture = externalFuture; } @@ -166,7 +166,7 @@ public void setExternalFuture(RetryingFuture externalFuture) { * return of this method should just be ignored. */ @Override - public Void call() { + public MutateRowsAttemptResult call() { try { // externalFuture is set from MutateRowsRetryingCallable before invoking this method. It // shouldn't be null unless the code changed @@ -192,7 +192,7 @@ public Void call() { } // Handle concurrent cancellation - externalFuture.setAttemptFuture(new NonCancellableFuture()); + externalFuture.setAttemptFuture(new NonCancellableFuture<>()); if (externalFuture.isDone()) { return null; } @@ -208,13 +208,13 @@ public Void call() { // Inspect the results and either propagate the success, or prepare to retry the failed // mutations - ApiFuture transformed = + ApiFuture transformed = ApiFutures.transform(catching, attemptSuccessfulCallback, MoreExecutors.directExecutor()); // Notify the parent of the attempt externalFuture.setAttemptFuture(transformed); } catch (Throwable e) { - externalFuture.setAttemptFuture(ApiFutures.immediateFailedFuture(e)); + externalFuture.setAttemptFuture(ApiFutures.immediateFailedFuture(e)); } return null; @@ -257,7 +257,8 @@ private void handleAttemptError(Throwable rpcError) { currentRequest = builder.build(); originalIndexes = newOriginalIndexes; - throw MutateRowsException.create(rpcError, allFailures.build(), builder.getEntriesCount() > 0); + throw MutateRowsException.create( + rpcError, entryError.getStatusCode(), allFailures.build(), builder.getEntriesCount() > 0); } /** @@ -267,7 +268,7 @@ private void handleAttemptError(Throwable rpcError) { * {@link MutateRowsException}. If no errors exist, then the attempt future is successfully * completed. We don't currently handle RetryInfo on entry level failures. */ - private void handleAttemptSuccess(List responses) { + private MutateRowsAttemptResult handleAttemptSuccess(List responses) { List allFailures = Lists.newArrayList(permanentFailures); MutateRowsRequest lastRequest = currentRequest; @@ -326,8 +327,9 @@ private void handleAttemptSuccess(List responses) { if (!allFailures.isEmpty()) { boolean isRetryable = builder.getEntriesCount() > 0; - throw MutateRowsException.create(null, allFailures, isRetryable); + return MutateRowsAttemptResult.create(allFailures, isRetryable); } + return MutateRowsAttemptResult.success(); } /** diff --git a/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/stub/mutaterows/MutateRowsAttemptResult.java b/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/stub/mutaterows/MutateRowsAttemptResult.java new file mode 100644 index 0000000000..d668c2a50f --- /dev/null +++ b/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/stub/mutaterows/MutateRowsAttemptResult.java @@ -0,0 +1,49 @@ +/* + * Copyright 2024 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.stub.mutaterows; + +import com.google.api.core.InternalApi; +import com.google.auto.value.AutoValue; +import com.google.cloud.bigtable.data.v2.models.MutateRowsException.FailedMutation; +import java.util.ArrayList; +import java.util.List; +import javax.annotation.Nonnull; + +/** + * This class represents the result of a MutateRows attempt. It contains a potentially empty list of + * failed mutations, along with an indicator whether these errors are retryable. + */ +@InternalApi +@AutoValue +public abstract class MutateRowsAttemptResult { + + public abstract List getFailedMutations(); + + public abstract boolean getIsRetryable(); + + @InternalApi + @Nonnull + public static MutateRowsAttemptResult create( + List failedMutations, boolean isRetryable) { + return new AutoValue_MutateRowsAttemptResult(failedMutations, isRetryable); + } + + @InternalApi + @Nonnull + public static MutateRowsAttemptResult success() { + return new AutoValue_MutateRowsAttemptResult(new ArrayList<>(), false); + } +} diff --git a/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/stub/mutaterows/MutateRowsBatchingDescriptor.java b/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/stub/mutaterows/MutateRowsBatchingDescriptor.java index 65cc781169..87f5c88d3e 100644 --- a/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/stub/mutaterows/MutateRowsBatchingDescriptor.java +++ b/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/stub/mutaterows/MutateRowsBatchingDescriptor.java @@ -37,7 +37,7 @@ */ @InternalApi("For internal use only") public class MutateRowsBatchingDescriptor - implements BatchingDescriptor { + implements BatchingDescriptor { @Override public BatchingRequestBuilder newRequestBuilder( @@ -46,7 +46,15 @@ public BatchingRequestBuilder newRequestBuilder( } @Override - public void splitResponse(Void response, List> entries) { + public void splitResponse( + MutateRowsAttemptResult response, List> entries) { + // For every failed mutation in the response, we set the exception on the matching requested + // mutation. It is important to set the correct error on the correct mutation. When the entry is + // later read, it resolves the exception first, and only later it goes to the value set by + // set(). + for (FailedMutation mutation : response.getFailedMutations()) { + entries.get(mutation.getIndex()).getResultFuture().setException(mutation.getError()); + } for (BatchEntry batchResponse : entries) { batchResponse.getResultFuture().set(null); } diff --git a/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/stub/mutaterows/MutateRowsPartialErrorRetryAlgorithm.java b/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/stub/mutaterows/MutateRowsPartialErrorRetryAlgorithm.java new file mode 100644 index 0000000000..9c7035db96 --- /dev/null +++ b/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/stub/mutaterows/MutateRowsPartialErrorRetryAlgorithm.java @@ -0,0 +1,79 @@ +/* + * Copyright 2024 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.stub.mutaterows; + +import com.google.api.core.InternalApi; +import com.google.api.gax.retrying.ResultRetryAlgorithmWithContext; +import com.google.api.gax.retrying.RetryingContext; +import com.google.api.gax.retrying.TimedAttemptSettings; +import org.checkerframework.checker.nullness.qual.Nullable; + +/** + * This algorithm will retry if there was a retryable failed mutation, or if there wasn't but the + * underlying algorithm allows a retry. + */ +@InternalApi +public class MutateRowsPartialErrorRetryAlgorithm + implements ResultRetryAlgorithmWithContext { + private final ResultRetryAlgorithmWithContext retryAlgorithm; + + public MutateRowsPartialErrorRetryAlgorithm( + ResultRetryAlgorithmWithContext retryAlgorithm) { + this.retryAlgorithm = retryAlgorithm; + } + + @Override + public boolean shouldRetry( + Throwable previousThrowable, MutateRowsAttemptResult previousResponse) { + // handle partial retryable failures + if (previousResponse != null && !previousResponse.getFailedMutations().isEmpty()) { + return previousResponse.getIsRetryable(); + } + // business as usual + return retryAlgorithm.shouldRetry(previousThrowable, previousResponse); + } + + @Override + public boolean shouldRetry( + @Nullable RetryingContext context, + Throwable previousThrowable, + MutateRowsAttemptResult previousResponse) { + // handle partial retryable failures + if (previousResponse != null && !previousResponse.getFailedMutations().isEmpty()) { + return previousResponse.getIsRetryable(); + } + // business as usual + return retryAlgorithm.shouldRetry(context, previousThrowable, previousResponse); + } + + @Override + public TimedAttemptSettings createNextAttempt( + Throwable previousThrowable, + MutateRowsAttemptResult previousResponse, + TimedAttemptSettings previousSettings) { + return retryAlgorithm.createNextAttempt(previousThrowable, previousResponse, previousSettings); + } + + @Override + public TimedAttemptSettings createNextAttempt( + RetryingContext context, + Throwable previousThrowable, + MutateRowsAttemptResult previousResponse, + TimedAttemptSettings previousSettings) { + return retryAlgorithm.createNextAttempt( + context, previousThrowable, previousResponse, previousSettings); + } +} diff --git a/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/stub/mutaterows/MutateRowsRetryingCallable.java b/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/stub/mutaterows/MutateRowsRetryingCallable.java index 8ad1db258d..354a5ea54a 100644 --- a/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/stub/mutaterows/MutateRowsRetryingCallable.java +++ b/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/stub/mutaterows/MutateRowsRetryingCallable.java @@ -40,17 +40,18 @@ * @see MutateRowsAttemptCallable for more details. */ @InternalApi -public class MutateRowsRetryingCallable extends UnaryCallable { +public class MutateRowsRetryingCallable + extends UnaryCallable { private final ApiCallContext callContextPrototype; private final ServerStreamingCallable callable; - private final RetryingExecutorWithContext executor; + private final RetryingExecutorWithContext executor; private final ImmutableSet retryCodes; private final RetryAlgorithm retryAlgorithm; public MutateRowsRetryingCallable( @Nonnull ApiCallContext callContextPrototype, @Nonnull ServerStreamingCallable callable, - @Nonnull RetryingExecutorWithContext executor, + @Nonnull RetryingExecutorWithContext executor, @Nonnull Set retryCodes, @Nonnull RetryAlgorithm retryAlgorithm) { this.callContextPrototype = Preconditions.checkNotNull(callContextPrototype); @@ -61,12 +62,14 @@ public MutateRowsRetryingCallable( } @Override - public RetryingFuture futureCall(MutateRowsRequest request, ApiCallContext inputContext) { + public RetryingFuture futureCall( + MutateRowsRequest request, ApiCallContext inputContext) { ApiCallContext context = callContextPrototype.nullToSelf(inputContext); MutateRowsAttemptCallable retryCallable = new MutateRowsAttemptCallable(callable.all(), request, context, retryCodes, retryAlgorithm); - RetryingFuture retryingFuture = executor.createFuture(retryCallable, context); + RetryingFuture retryingFuture = + executor.createFuture(retryCallable, context); retryCallable.setExternalFuture(retryingFuture); retryCallable.call(); diff --git a/google-cloud-bigtable/src/test/java/com/google/cloud/bigtable/admin/v2/BigtableTableAdminClientTests.java b/google-cloud-bigtable/src/test/java/com/google/cloud/bigtable/admin/v2/BigtableTableAdminClientTests.java index 86afadd237..e604495c43 100644 --- a/google-cloud-bigtable/src/test/java/com/google/cloud/bigtable/admin/v2/BigtableTableAdminClientTests.java +++ b/google-cloud-bigtable/src/test/java/com/google/cloud/bigtable/admin/v2/BigtableTableAdminClientTests.java @@ -27,11 +27,13 @@ import com.google.api.gax.rpc.OperationCallable; import com.google.api.gax.rpc.UnaryCallable; import com.google.api.gax.rpc.testing.FakeOperationSnapshot; +import com.google.bigtable.admin.v2.AuthorizedViewName; import com.google.bigtable.admin.v2.Backup.State; import com.google.bigtable.admin.v2.BackupInfo; import com.google.bigtable.admin.v2.ChangeStreamConfig; import com.google.bigtable.admin.v2.ColumnFamily; import com.google.bigtable.admin.v2.CopyBackupMetadata; +import com.google.bigtable.admin.v2.CreateAuthorizedViewMetadata; import com.google.bigtable.admin.v2.CreateBackupMetadata; import com.google.bigtable.admin.v2.DeleteBackupRequest; import com.google.bigtable.admin.v2.DeleteTableRequest; @@ -47,25 +49,32 @@ import com.google.bigtable.admin.v2.Table.ClusterState; import com.google.bigtable.admin.v2.Table.View; import com.google.bigtable.admin.v2.TableName; +import com.google.bigtable.admin.v2.UpdateAuthorizedViewMetadata; import com.google.bigtable.admin.v2.UpdateTableMetadata; import com.google.cloud.Identity; import com.google.cloud.Policy; import com.google.cloud.Role; +import com.google.cloud.bigtable.admin.v2.BaseBigtableTableAdminClient.ListAuthorizedViewsPage; +import com.google.cloud.bigtable.admin.v2.BaseBigtableTableAdminClient.ListAuthorizedViewsPagedResponse; import com.google.cloud.bigtable.admin.v2.BaseBigtableTableAdminClient.ListBackupsPage; import com.google.cloud.bigtable.admin.v2.BaseBigtableTableAdminClient.ListBackupsPagedResponse; import com.google.cloud.bigtable.admin.v2.BaseBigtableTableAdminClient.ListTablesPage; import com.google.cloud.bigtable.admin.v2.BaseBigtableTableAdminClient.ListTablesPagedResponse; import com.google.cloud.bigtable.admin.v2.internal.NameUtil; +import com.google.cloud.bigtable.admin.v2.models.AuthorizedView; import com.google.cloud.bigtable.admin.v2.models.Backup; import com.google.cloud.bigtable.admin.v2.models.CopyBackupRequest; +import com.google.cloud.bigtable.admin.v2.models.CreateAuthorizedViewRequest; import com.google.cloud.bigtable.admin.v2.models.CreateBackupRequest; import com.google.cloud.bigtable.admin.v2.models.CreateTableRequest; import com.google.cloud.bigtable.admin.v2.models.EncryptionInfo; import com.google.cloud.bigtable.admin.v2.models.ModifyColumnFamiliesRequest; import com.google.cloud.bigtable.admin.v2.models.RestoreTableRequest; import com.google.cloud.bigtable.admin.v2.models.RestoredTableResult; +import com.google.cloud.bigtable.admin.v2.models.SubsetView; import com.google.cloud.bigtable.admin.v2.models.Table; import com.google.cloud.bigtable.admin.v2.models.Type; +import com.google.cloud.bigtable.admin.v2.models.UpdateAuthorizedViewRequest; import com.google.cloud.bigtable.admin.v2.models.UpdateBackupRequest; import com.google.cloud.bigtable.admin.v2.stub.EnhancedBigtableTableAdminStub; import com.google.common.collect.ImmutableList; @@ -75,6 +84,7 @@ import com.google.protobuf.ByteString; import com.google.protobuf.Duration; import com.google.protobuf.Empty; +import com.google.protobuf.FieldMask; import com.google.protobuf.Timestamp; import com.google.protobuf.util.Timestamps; import io.grpc.Status; @@ -110,6 +120,7 @@ public class BigtableTableAdminClientTests { private static final String TABLE_ID = "my-table"; private static final String CLUSTER_ID = "my-cluster"; private static final String BACKUP_ID = "my-backup"; + private static final String AUTHORIZED_VIEW_ID = "my-authorized-view"; private static final String INSTANCE_NAME = NameUtil.formatInstanceName(PROJECT_ID, INSTANCE_ID); private static final String TABLE_NAME = @@ -182,6 +193,35 @@ public class BigtableTableAdminClientTests { CopyBackupMetadata> mockCopyBackupOperationCallable; + @Mock + private OperationCallable< + com.google.bigtable.admin.v2.CreateAuthorizedViewRequest, + com.google.bigtable.admin.v2.AuthorizedView, + CreateAuthorizedViewMetadata> + mockCreateAuthorizedViewOperationCallable; + + @Mock + private OperationCallable< + com.google.bigtable.admin.v2.UpdateAuthorizedViewRequest, + com.google.bigtable.admin.v2.AuthorizedView, + UpdateAuthorizedViewMetadata> + mockUpdateAuthorizedViewOperationCallable; + + @Mock + private UnaryCallable< + com.google.bigtable.admin.v2.GetAuthorizedViewRequest, + com.google.bigtable.admin.v2.AuthorizedView> + mockGetAuthorizedViewCallable; + + @Mock + private UnaryCallable< + com.google.bigtable.admin.v2.ListAuthorizedViewsRequest, ListAuthorizedViewsPagedResponse> + mockListAuthorizedViewsCallable; + + @Mock + private UnaryCallable + mockDeleteAuthorizedViewCallable; + @Mock private UnaryCallable mockGetIamPolicyCallable; @@ -877,6 +917,227 @@ public void testCopyBackup() { assertThat(actualResult.getSizeBytes()).isEqualTo(sizeBytes); } + @Test + public void testCreateAuthorizedView() { + // Setup + Mockito.when(mockStub.createAuthorizedViewOperationCallable()) + .thenReturn(mockCreateAuthorizedViewOperationCallable); + + com.google.bigtable.admin.v2.CreateAuthorizedViewRequest expectedRequest = + com.google.bigtable.admin.v2.CreateAuthorizedViewRequest.newBuilder() + .setParent(NameUtil.formatTableName(PROJECT_ID, INSTANCE_ID, TABLE_ID)) + .setAuthorizedViewId(AUTHORIZED_VIEW_ID) + .setAuthorizedView( + com.google.bigtable.admin.v2.AuthorizedView.newBuilder() + .setSubsetView( + com.google.bigtable.admin.v2.AuthorizedView.SubsetView.newBuilder() + .addRowPrefixes(ByteString.copyFromUtf8("row#")) + .build()) + .setDeletionProtection(true) + .build()) + .build(); + + com.google.bigtable.admin.v2.AuthorizedView expectedResponse = + com.google.bigtable.admin.v2.AuthorizedView.newBuilder() + .setName( + NameUtil.formatAuthorizedViewName( + PROJECT_ID, INSTANCE_ID, TABLE_ID, AUTHORIZED_VIEW_ID)) + .setSubsetView( + com.google.bigtable.admin.v2.AuthorizedView.SubsetView.newBuilder() + .addRowPrefixes(ByteString.copyFromUtf8("row#")) + .build()) + .setDeletionProtection(true) + .build(); + + mockOperationResult( + mockCreateAuthorizedViewOperationCallable, + expectedRequest, + expectedResponse, + CreateAuthorizedViewMetadata.newBuilder().setOriginalRequest(expectedRequest).build()); + + CreateAuthorizedViewRequest req = + CreateAuthorizedViewRequest.of(TABLE_ID, AUTHORIZED_VIEW_ID) + .setDeletionProtection(true) + .setAuthorizedViewType(SubsetView.create().addRowPrefix("row#")); + + // Execute + AuthorizedView actualResult = adminClient.createAuthorizedView(req); + + // Verify + assertThat(actualResult).isEqualTo(AuthorizedView.fromProto(expectedResponse)); + } + + @Test + public void testUpdateAuthorizedView() { + // Setup + Mockito.when(mockStub.updateAuthorizedViewOperationCallable()) + .thenReturn(mockUpdateAuthorizedViewOperationCallable); + + com.google.bigtable.admin.v2.UpdateAuthorizedViewRequest expectedRequest = + com.google.bigtable.admin.v2.UpdateAuthorizedViewRequest.newBuilder() + .setAuthorizedView( + com.google.bigtable.admin.v2.AuthorizedView.newBuilder() + .setName( + NameUtil.formatAuthorizedViewName( + PROJECT_ID, INSTANCE_ID, TABLE_ID, AUTHORIZED_VIEW_ID)) + .setSubsetView( + com.google.bigtable.admin.v2.AuthorizedView.SubsetView.newBuilder() + .addRowPrefixes(ByteString.copyFromUtf8("row#")) + .build()) + .setDeletionProtection(true) + .build()) + .setUpdateMask( + FieldMask.newBuilder().addPaths("deletion_protection").addPaths("subset_view")) + .build(); + + com.google.bigtable.admin.v2.AuthorizedView expectedResponse = + com.google.bigtable.admin.v2.AuthorizedView.newBuilder() + .setName( + NameUtil.formatAuthorizedViewName( + PROJECT_ID, INSTANCE_ID, TABLE_ID, AUTHORIZED_VIEW_ID)) + .setSubsetView( + com.google.bigtable.admin.v2.AuthorizedView.SubsetView.newBuilder() + .addRowPrefixes(ByteString.copyFromUtf8("row#")) + .build()) + .setDeletionProtection(true) + .build(); + + mockOperationResult( + mockUpdateAuthorizedViewOperationCallable, + expectedRequest, + expectedResponse, + UpdateAuthorizedViewMetadata.newBuilder().setOriginalRequest(expectedRequest).build()); + + UpdateAuthorizedViewRequest req = + UpdateAuthorizedViewRequest.of(TABLE_ID, AUTHORIZED_VIEW_ID) + .setDeletionProtection(true) + .setAuthorizedViewType(SubsetView.create().addRowPrefix("row#")); + + // Execute + AuthorizedView actualResult = adminClient.updateAuthorizedView(req); + + // Verify + assertThat(actualResult).isEqualTo(AuthorizedView.fromProto(expectedResponse)); + } + + @Test + public void testGetAuthorizedView() { + // Setup + Mockito.when(mockStub.getAuthorizedViewCallable()).thenReturn(mockGetAuthorizedViewCallable); + + com.google.bigtable.admin.v2.GetAuthorizedViewRequest expectedRequest = + com.google.bigtable.admin.v2.GetAuthorizedViewRequest.newBuilder() + .setName( + NameUtil.formatAuthorizedViewName( + PROJECT_ID, INSTANCE_ID, TABLE_ID, AUTHORIZED_VIEW_ID)) + .build(); + + com.google.bigtable.admin.v2.AuthorizedView expectedResponse = + com.google.bigtable.admin.v2.AuthorizedView.newBuilder() + .setName( + NameUtil.formatAuthorizedViewName( + PROJECT_ID, INSTANCE_ID, TABLE_ID, AUTHORIZED_VIEW_ID)) + .setSubsetView( + com.google.bigtable.admin.v2.AuthorizedView.SubsetView.newBuilder() + .addRowPrefixes(ByteString.copyFromUtf8("row#")) + .build()) + .setDeletionProtection(true) + .build(); + + Mockito.when(mockGetAuthorizedViewCallable.futureCall(expectedRequest)) + .thenReturn(ApiFutures.immediateFuture(expectedResponse)); + + // Execute + AuthorizedView actualResult = adminClient.getAuthorizedView(TABLE_ID, AUTHORIZED_VIEW_ID); + + // Verify + assertThat(actualResult).isEqualTo(AuthorizedView.fromProto(expectedResponse)); + } + + @Test + public void testListAuthorizedViews() { + // Setup + Mockito.when(mockStub.listAuthorizedViewsPagedCallable()) + .thenReturn(mockListAuthorizedViewsCallable); + + com.google.bigtable.admin.v2.ListAuthorizedViewsRequest expectedRequest = + com.google.bigtable.admin.v2.ListAuthorizedViewsRequest.newBuilder() + .setParent(NameUtil.formatTableName(PROJECT_ID, INSTANCE_ID, TABLE_ID)) + .build(); + + // 3 AuthorizedViews spread across 2 pages + List expectedProtos = Lists.newArrayList(); + for (int i = 0; i < 3; i++) { + expectedProtos.add( + com.google.bigtable.admin.v2.AuthorizedView.newBuilder() + .setName( + NameUtil.formatAuthorizedViewName( + PROJECT_ID, INSTANCE_ID, TABLE_ID, AUTHORIZED_VIEW_ID + i)) + .build()); + } + + // 2 on the first page + ListAuthorizedViewsPage page0 = Mockito.mock(ListAuthorizedViewsPage.class); + Mockito.when(page0.getValues()).thenReturn(expectedProtos.subList(0, 2)); + Mockito.when(page0.hasNextPage()).thenReturn(true); + + // 1 on the last page + ListAuthorizedViewsPage page1 = Mockito.mock(ListAuthorizedViewsPage.class); + Mockito.when(page1.getValues()).thenReturn(expectedProtos.subList(2, 3)); + + // Link page0 to page1 + Mockito.when(page0.getNextPageAsync()).thenReturn(ApiFutures.immediateFuture(page1)); + + // Link page to the response + ListAuthorizedViewsPagedResponse response0 = + Mockito.mock(ListAuthorizedViewsPagedResponse.class); + Mockito.when(response0.getPage()).thenReturn(page0); + + Mockito.when(mockListAuthorizedViewsCallable.futureCall(expectedRequest)) + .thenReturn(ApiFutures.immediateFuture(response0)); + + // Execute + List actualResults = adminClient.listAuthorizedViews(TABLE_ID); + + // Verify + List expectedResults = Lists.newArrayList(); + for (com.google.bigtable.admin.v2.AuthorizedView expectedProto : expectedProtos) { + expectedResults.add(AuthorizedViewName.parse(expectedProto.getName()).getAuthorizedView()); + } + + assertThat(actualResults).containsExactlyElementsIn(expectedResults); + } + + @Test + public void testDeleteAuthorizedView() { + // Setup + Mockito.when(mockStub.deleteAuthorizedViewCallable()) + .thenReturn(mockDeleteAuthorizedViewCallable); + + com.google.bigtable.admin.v2.DeleteAuthorizedViewRequest expectedRequest = + com.google.bigtable.admin.v2.DeleteAuthorizedViewRequest.newBuilder() + .setName( + NameUtil.formatAuthorizedViewName( + PROJECT_ID, INSTANCE_ID, TABLE_ID, AUTHORIZED_VIEW_ID)) + .build(); + + final AtomicBoolean wasCalled = new AtomicBoolean(false); + + Mockito.when(mockDeleteAuthorizedViewCallable.futureCall(expectedRequest)) + .thenAnswer( + (Answer>) + invocationOnMock -> { + wasCalled.set(true); + return ApiFutures.immediateFuture(Empty.getDefaultInstance()); + }); + + // Execute + adminClient.deleteAuthorizedView(TABLE_ID, AUTHORIZED_VIEW_ID); + + // Verify + assertThat(wasCalled.get()).isTrue(); + } + @Test public void testGetBackupIamPolicy() { // Setup diff --git a/google-cloud-bigtable/src/test/java/com/google/cloud/bigtable/admin/v2/internal/NameUtilTest.java b/google-cloud-bigtable/src/test/java/com/google/cloud/bigtable/admin/v2/internal/NameUtilTest.java index a452a2bc55..7622ce5dfa 100644 --- a/google-cloud-bigtable/src/test/java/com/google/cloud/bigtable/admin/v2/internal/NameUtilTest.java +++ b/google-cloud-bigtable/src/test/java/com/google/cloud/bigtable/admin/v2/internal/NameUtilTest.java @@ -17,6 +17,8 @@ import static com.google.common.truth.Truth.assertThat; +import com.google.cloud.bigtable.data.v2.models.AuthorizedViewId; +import com.google.cloud.bigtable.data.v2.models.TableId; import org.junit.Rule; import org.junit.Test; import org.junit.rules.ExpectedException; @@ -45,4 +47,78 @@ public void formatBackupNameTest() { assertThat(NameUtil.formatBackupName("my-project", "my-instance", "my-cluster", "my-backup")) .isEqualTo(testBackupName); } + + @Test + public void formatAuthorizedViewNameTest() { + String testAuthorizedViewName = + "projects/my-project/instances/my-instance/tables/my-table/authorizedViews/my-authorized-view"; + + assertThat( + NameUtil.formatAuthorizedViewName( + "my-project", "my-instance", "my-table", "my-authorized-view")) + .isEqualTo(testAuthorizedViewName); + } + + @Test + public void extractAuthorizedViewIdFromAuthorizedViewNameTest() { + String testAuthorizedViewName = + "projects/my-project/instances/my-instance/tables/my-table/authorizedViews/my-authorized-view"; + + assertThat(NameUtil.extractAuthorizedViewIdFromAuthorizedViewName(testAuthorizedViewName)) + .isEqualTo("my-authorized-view"); + + exception.expect(IllegalArgumentException.class); + NameUtil.extractAuthorizedViewIdFromAuthorizedViewName("bad-format"); + } + + @Test + public void extractTableIdFromAuthorizedViewNameTest() { + String testAuthorizedViewName = + "projects/my-project/instances/my-instance/tables/my-table/authorizedViews/my-authorized-view"; + + assertThat( + com.google.cloud.bigtable.data.v2.internal.NameUtil + .extractTableIdFromAuthorizedViewName(testAuthorizedViewName)) + .isEqualTo("my-table"); + + exception.expect(IllegalArgumentException.class); + com.google.cloud.bigtable.data.v2.internal.NameUtil.extractTableIdFromAuthorizedViewName( + "bad-format"); + } + + @Test + public void extractTableNameFromAuthorizedViewNameTest() { + String testAuthorizedViewName = + "projects/my-project/instances/my-instance/tables/my-table/authorizedViews/my-authorized-view"; + + assertThat( + com.google.cloud.bigtable.data.v2.internal.NameUtil + .extractTableNameFromAuthorizedViewName(testAuthorizedViewName)) + .isEqualTo("projects/my-project/instances/my-instance/tables/my-table"); + + exception.expect(IllegalArgumentException.class); + com.google.cloud.bigtable.data.v2.internal.NameUtil.extractTableNameFromAuthorizedViewName( + "bad-format"); + } + + @Test + public void testExtractTargetId() { + String testTableName = "projects/my-project/instances/my-instance/tables/my-table"; + String testAuthorizedViewName = + "projects/my-project/instances/my-instance/tables/my-table/authorizedViews/my-authorized-view"; + assertThat( + com.google.cloud.bigtable.data.v2.internal.NameUtil.extractTargetId(testTableName, "")) + .isEqualTo(TableId.of("my-table")); + assertThat( + com.google.cloud.bigtable.data.v2.internal.NameUtil.extractTargetId( + "", testAuthorizedViewName)) + .isEqualTo(AuthorizedViewId.of("my-table", "my-authorized-view")); + + exception.expect(IllegalArgumentException.class); + com.google.cloud.bigtable.data.v2.internal.NameUtil.extractTargetId("", ""); + + exception.expect(IllegalArgumentException.class); + com.google.cloud.bigtable.data.v2.internal.NameUtil.extractTargetId( + testTableName, testAuthorizedViewName); + } } diff --git a/google-cloud-bigtable/src/test/java/com/google/cloud/bigtable/admin/v2/it/BigtableAuthorizedViewIT.java b/google-cloud-bigtable/src/test/java/com/google/cloud/bigtable/admin/v2/it/BigtableAuthorizedViewIT.java new file mode 100644 index 0000000000..eb23e0325a --- /dev/null +++ b/google-cloud-bigtable/src/test/java/com/google/cloud/bigtable/admin/v2/it/BigtableAuthorizedViewIT.java @@ -0,0 +1,275 @@ +/* + * Copyright 2024 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.admin.v2.it; + +import static com.google.common.truth.Truth.assertThat; +import static com.google.common.truth.Truth.assertWithMessage; +import static com.google.common.truth.TruthJUnit.assume; +import static org.junit.Assert.fail; + +import com.google.api.gax.batching.Batcher; +import com.google.api.gax.rpc.FailedPreconditionException; +import com.google.api.gax.rpc.NotFoundException; +import com.google.cloud.Policy; +import com.google.cloud.bigtable.admin.v2.BigtableTableAdminClient; +import com.google.cloud.bigtable.admin.v2.models.AuthorizedView; +import com.google.cloud.bigtable.admin.v2.models.CreateAuthorizedViewRequest; +import com.google.cloud.bigtable.admin.v2.models.CreateTableRequest; +import com.google.cloud.bigtable.admin.v2.models.FamilySubsets; +import com.google.cloud.bigtable.admin.v2.models.SubsetView; +import com.google.cloud.bigtable.admin.v2.models.Table; +import com.google.cloud.bigtable.admin.v2.models.UpdateAuthorizedViewRequest; +import com.google.cloud.bigtable.data.v2.BigtableDataClient; +import com.google.cloud.bigtable.data.v2.models.RowMutationEntry; +import com.google.cloud.bigtable.test_helpers.env.EmulatorEnv; +import com.google.cloud.bigtable.test_helpers.env.PrefixGenerator; +import com.google.cloud.bigtable.test_helpers.env.TestEnvRule; +import com.google.protobuf.ByteString; +import io.grpc.StatusRuntimeException; +import java.util.List; +import java.util.Random; +import java.util.logging.Logger; +import org.junit.AfterClass; +import org.junit.BeforeClass; +import org.junit.ClassRule; +import org.junit.Rule; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + +@RunWith(JUnit4.class) +public class BigtableAuthorizedViewIT { + @ClassRule public static final TestEnvRule testEnvRule = new TestEnvRule(); + @Rule public final PrefixGenerator prefixGenerator = new PrefixGenerator(); + private static final Logger LOGGER = Logger.getLogger(BigtableAuthorizedViewIT.class.getName()); + private static final int[] BACKOFF_DURATION = {2, 4, 8, 16, 32, 64, 128, 256, 512, 1024}; + + private static BigtableTableAdminClient tableAdmin; + private static BigtableDataClient dataClient; + private static Table testTable; + + @BeforeClass + public static void setUpClass() throws InterruptedException { + assume() + .withMessage("BigtableInstanceAdminClient is not supported on Emulator") + .that(testEnvRule.env()) + .isNotInstanceOf(EmulatorEnv.class); + + tableAdmin = testEnvRule.env().getTableAdminClient(); + dataClient = testEnvRule.env().getDataClient(); + + testTable = createAndPopulateTestTable(tableAdmin, dataClient); + } + + @AfterClass + public static void tearDownClass() { + if (testTable != null) { + try { + tableAdmin.deleteTable(testTable.getId()); + } catch (Exception e) { + // Ignore. + } + } + } + + @Test + public void createAuthorizedViewAndGetAuthorizedViewTest() { + String authorizedViewId = prefixGenerator.newPrefix(); + + CreateAuthorizedViewRequest request = + CreateAuthorizedViewRequest.of(testTable.getId(), authorizedViewId) + .setAuthorizedViewType( + SubsetView.create() + .addRowPrefix("row#") + .setFamilySubsets( + "cf1", + FamilySubsets.create() + .addQualifier("qualifier") + .addQualifierPrefix("prefix#"))) + .setDeletionProtection(false); + try { + AuthorizedView response = tableAdmin.createAuthorizedView(request); + assertWithMessage("Got wrong authorized view Id in CreateAuthorizedView") + .that(response.getId()) + .isEqualTo(authorizedViewId); + assertWithMessage("Got wrong deletion protection in CreateAuthorizedView") + .that(response.isDeletionProtected()) + .isFalse(); + assertWithMessage("Got wrong subset view in CreateAuthorizedView") + .that(((SubsetView) response.getAuthorizedViewType()).getRowPrefixes()) + .containsExactly(ByteString.copyFromUtf8("row#")); + assertWithMessage("Got wrong family subsets in CreateAuthorizedView") + .that(((SubsetView) response.getAuthorizedViewType()).getFamilySubsets()) + .containsExactly( + "cf1", + FamilySubsets.create().addQualifier("qualifier").addQualifierPrefix("prefix#")); + + response = tableAdmin.getAuthorizedView(testTable.getId(), authorizedViewId); + assertWithMessage("Got wrong authorized view Id in getAuthorizedView") + .that(response.getId()) + .isEqualTo(authorizedViewId); + assertWithMessage("Got wrong deletion protection in getAuthorizedView") + .that(response.isDeletionProtected()) + .isFalse(); + assertWithMessage("Got wrong subset view in getAuthorizedView") + .that(((SubsetView) response.getAuthorizedViewType()).getRowPrefixes()) + .containsExactly(ByteString.copyFromUtf8("row#")); + assertWithMessage("Got wrong family subsets in getAuthorizedView") + .that(((SubsetView) response.getAuthorizedViewType()).getFamilySubsets()) + .containsExactly( + "cf1", + FamilySubsets.create().addQualifier("qualifier").addQualifierPrefix("prefix#")); + } finally { + tableAdmin.deleteAuthorizedView(testTable.getId(), authorizedViewId); + } + } + + @Test + public void listAuthorizedViewsTest() { + String authorizedViewId1 = prefixGenerator.newPrefix(); + String authorizedViewId2 = prefixGenerator.newPrefix(); + + try { + tableAdmin.createAuthorizedView(createAuthorizedViewRequest(authorizedViewId1)); + tableAdmin.createAuthorizedView(createAuthorizedViewRequest(authorizedViewId2)); + + List response = tableAdmin.listAuthorizedViews(testTable.getId()); + // Concurrent tests running may cause flakiness. Use containsAtLeast instead of + // containsExactly. + assertWithMessage("Got wrong authorized view Ids in listAuthorizedViews") + .that(response) + .containsAtLeast( + tableAdmin.getAuthorizedView(testTable.getId(), authorizedViewId1).getId(), + tableAdmin.getAuthorizedView(testTable.getId(), authorizedViewId2).getId()); + } finally { + tableAdmin.deleteAuthorizedView(testTable.getId(), authorizedViewId1); + tableAdmin.deleteAuthorizedView(testTable.getId(), authorizedViewId2); + } + } + + @Test + public void updateAuthorizedViewAndDeleteAuthorizedViewTest() throws InterruptedException { + String authorizedViewId = prefixGenerator.newPrefix(); + + // Create a deletion-protected authorized view. + CreateAuthorizedViewRequest request = + createAuthorizedViewRequest(authorizedViewId).setDeletionProtection(true); + + AuthorizedView response = tableAdmin.createAuthorizedView(request); + assertWithMessage("Got wrong deletion protection in CreateAuthorizedView") + .that(response.isDeletionProtected()) + .isTrue(); + + // We should not be able to delete the authorized view. + try { + tableAdmin.deleteAuthorizedView(testTable.getId(), authorizedViewId); + fail("A delete-protected authorized view should not have been able to be deleted"); + } catch (FailedPreconditionException e) { + assertWithMessage("Incorrect exception type") + .that(e.getCause()) + .isInstanceOf(StatusRuntimeException.class); + } + + // Update the deletion protection bit of the authorized view. + UpdateAuthorizedViewRequest updateRequest = + UpdateAuthorizedViewRequest.of(testTable.getId(), authorizedViewId) + .setDeletionProtection(false); + response = tableAdmin.updateAuthorizedView(updateRequest); + assertWithMessage("Got wrong deletion protection in UpdateAuthorizedView") + .that(response.isDeletionProtected()) + .isFalse(); + + // Now we should be able to successfully delete the AuthorizedView. + tableAdmin.deleteAuthorizedView(testTable.getId(), authorizedViewId); + try { + for (int i = 0; i < BACKOFF_DURATION.length; i++) { + tableAdmin.getAuthorizedView(testTable.getId(), authorizedViewId); + + LOGGER.info( + "Wait for " + + BACKOFF_DURATION[i] + + " seconds for deleting authorized view " + + authorizedViewId); + Thread.sleep(BACKOFF_DURATION[i] * 1000); + } + fail("AuthorizedView was not deleted."); + } catch (NotFoundException e) { + assertWithMessage("Incorrect exception type") + .that(e.getCause()) + .isInstanceOf(StatusRuntimeException.class); + } + } + + @Test + public void authorizedViewIamTest() { + String authorizedViewId = prefixGenerator.newPrefix(); + + try { + tableAdmin.createAuthorizedView(createAuthorizedViewRequest(authorizedViewId)); + Policy policy = tableAdmin.getAuthorizedViewIamPolicy(testTable.getId(), authorizedViewId); + assertThat(policy).isNotNull(); + + Exception actualEx = null; + try { + assertThat( + tableAdmin.setAuthorizedViewIamPolicy(testTable.getId(), authorizedViewId, policy)) + .isNotNull(); + } catch (Exception iamException) { + actualEx = iamException; + } + assertThat(actualEx).isNull(); + + List permissions = + tableAdmin.testAuthorizedViewIamPermission( + testTable.getId(), + authorizedViewId, + "bigtable.authorizedViews.get", + "bigtable.authorizedViews.update", + "bigtable.authorizedViews.delete"); + assertThat(permissions).hasSize(3); + } finally { + tableAdmin.deleteAuthorizedView(testTable.getId(), authorizedViewId); + } + } + + private CreateAuthorizedViewRequest createAuthorizedViewRequest(String authorizedViewId) { + return CreateAuthorizedViewRequest.of(testTable.getId(), authorizedViewId) + .setAuthorizedViewType(SubsetView.create()); + } + + private static Table createAndPopulateTestTable( + BigtableTableAdminClient tableAdmin, BigtableDataClient dataClient) + throws InterruptedException { + String tableId = + PrefixGenerator.newPrefix("BigtableAuthorizedViewIT#createAndPopulateTestTable"); + Table testTable = tableAdmin.createTable(CreateTableRequest.of(tableId).addFamily("cf1")); + + // Populate test data. + byte[] rowBytes = new byte[1024]; + Random random = new Random(); + random.nextBytes(rowBytes); + + try (Batcher batcher = dataClient.newBulkMutationBatcher(tableId)) { + for (int i = 0; i < 10; i++) { + batcher.add( + RowMutationEntry.create("test-row-" + i) + .setCell("cf1", ByteString.EMPTY, ByteString.copyFrom(rowBytes))); + } + } + return testTable; + } +} diff --git a/google-cloud-bigtable/src/test/java/com/google/cloud/bigtable/admin/v2/models/AuthorizedViewTest.java b/google-cloud-bigtable/src/test/java/com/google/cloud/bigtable/admin/v2/models/AuthorizedViewTest.java new file mode 100644 index 0000000000..6eace3ee97 --- /dev/null +++ b/google-cloud-bigtable/src/test/java/com/google/cloud/bigtable/admin/v2/models/AuthorizedViewTest.java @@ -0,0 +1,184 @@ +/* + * Copyright 2024 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.admin.v2.models; + +import static com.google.common.truth.Truth.assertThat; + +import com.google.bigtable.admin.v2.AuthorizedViewName; +import com.google.protobuf.ByteString; +import java.util.Map; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + +@RunWith(JUnit4.class) +public class AuthorizedViewTest { + private static final String PROJECT_ID = "my-project"; + private static final String INSTANCE_ID = "my-instance"; + private static final String TABLE_ID = "my-table"; + private static final String AUTHORIZED_VIEW_ID = "my-authorized-view"; + + @Test + public void testFromProto() { + AuthorizedViewName authorizedViewName = + AuthorizedViewName.of(PROJECT_ID, INSTANCE_ID, TABLE_ID, AUTHORIZED_VIEW_ID); + + com.google.bigtable.admin.v2.AuthorizedView.SubsetView subsetViewProto = + com.google.bigtable.admin.v2.AuthorizedView.SubsetView.newBuilder() + .addRowPrefixes(ByteString.copyFromUtf8("row1#")) + .addRowPrefixes(ByteString.copyFromUtf8("row2#")) + .putFamilySubsets( + "family1", + com.google.bigtable.admin.v2.AuthorizedView.FamilySubsets.newBuilder() + .addQualifiers(ByteString.copyFromUtf8("column1")) + .addQualifiers(ByteString.copyFromUtf8("column2")) + .addQualifierPrefixes(ByteString.copyFromUtf8("column3#")) + .addQualifierPrefixes(ByteString.copyFromUtf8("column4#")) + .build()) + .putFamilySubsets( + "family2", + com.google.bigtable.admin.v2.AuthorizedView.FamilySubsets.newBuilder() + .addQualifiers(ByteString.copyFromUtf8("column5")) + .addQualifierPrefixes(ByteString.copyFromUtf8("")) + .build()) + .build(); + + com.google.bigtable.admin.v2.AuthorizedView authorizedViewProto = + com.google.bigtable.admin.v2.AuthorizedView.newBuilder() + .setName(authorizedViewName.toString()) + .setDeletionProtection(true) + .setSubsetView(subsetViewProto) + .build(); + + AuthorizedView result = AuthorizedView.fromProto(authorizedViewProto); + + assertThat(result.getId()).isEqualTo(AUTHORIZED_VIEW_ID); + assertThat(result.getTableId()).isEqualTo(TABLE_ID); + assertThat(result.isDeletionProtected()).isTrue(); + SubsetView subsetViewResult = (SubsetView) result.getAuthorizedViewType(); + assertThat(subsetViewResult).isEqualTo(SubsetView.fromProto(subsetViewProto)); + assertThat(subsetViewResult.getRowPrefixes()) + .containsExactly(ByteString.copyFromUtf8("row1#"), ByteString.copyFromUtf8("row2#")); + + Map familySubsetsResult = subsetViewResult.getFamilySubsets(); + assertThat(familySubsetsResult) + .containsExactly( + "family1", + FamilySubsets.fromProto(subsetViewProto.getFamilySubsetsOrThrow("family1")), + "family2", + FamilySubsets.fromProto(subsetViewProto.getFamilySubsetsOrThrow("family2"))); + assertThat(familySubsetsResult.get("family1").getQualifiers()) + .containsExactly(ByteString.copyFromUtf8("column1"), ByteString.copyFromUtf8("column2")); + assertThat(familySubsetsResult.get("family1").getQualifierPrefixes()) + .containsExactly(ByteString.copyFromUtf8("column3#"), ByteString.copyFromUtf8("column4#")); + assertThat(familySubsetsResult.get("family2").getQualifiers()) + .containsExactly(ByteString.copyFromUtf8("column5")); + assertThat(familySubsetsResult.get("family2").getQualifierPrefixes()) + .containsExactly(ByteString.copyFromUtf8("")); + } + + @Test + public void testRequiresName() { + com.google.bigtable.admin.v2.AuthorizedView proto = + com.google.bigtable.admin.v2.AuthorizedView.newBuilder() + .setDeletionProtection(true) + .setSubsetView( + com.google.bigtable.admin.v2.AuthorizedView.SubsetView.newBuilder().build()) + .build(); + + Exception actualException = null; + + try { + AuthorizedView.fromProto(proto); + } catch (Exception e) { + actualException = e; + } + + assertThat(actualException).isInstanceOf(IllegalArgumentException.class); + } + + @Test + public void testRequiresAuthorizedViewType() { + AuthorizedViewName authorizedViewName = + AuthorizedViewName.of(PROJECT_ID, INSTANCE_ID, TABLE_ID, AUTHORIZED_VIEW_ID); + com.google.bigtable.admin.v2.AuthorizedView proto = + com.google.bigtable.admin.v2.AuthorizedView.newBuilder() + .setName(authorizedViewName.toString()) + .setDeletionProtection(true) + .build(); + Exception actualException = null; + + try { + AuthorizedView.fromProto(proto); + } catch (Exception e) { + actualException = e; + } + + assertThat(actualException).isInstanceOf(IllegalArgumentException.class); + } + + @Test + public void testEquality() { + AuthorizedViewName authorizedViewName = + AuthorizedViewName.of(PROJECT_ID, INSTANCE_ID, TABLE_ID, AUTHORIZED_VIEW_ID); + com.google.bigtable.admin.v2.AuthorizedView proto = + com.google.bigtable.admin.v2.AuthorizedView.newBuilder() + .setName(authorizedViewName.toString()) + .setDeletionProtection(true) + .setSubsetView( + com.google.bigtable.admin.v2.AuthorizedView.SubsetView.newBuilder().build()) + .build(); + AuthorizedView authorizedView = AuthorizedView.fromProto(proto); + + assertThat(authorizedView).isEqualTo(AuthorizedView.fromProto(proto)); + + assertThat(authorizedView) + .isNotEqualTo( + com.google.bigtable.admin.v2.AuthorizedView.newBuilder() + .setName(authorizedViewName.toString()) + .setDeletionProtection(false) + .setSubsetView( + com.google.bigtable.admin.v2.AuthorizedView.SubsetView.newBuilder().build()) + .build()); + } + + @Test + public void testHashCode() { + AuthorizedViewName authorizedViewName = + AuthorizedViewName.of(PROJECT_ID, INSTANCE_ID, TABLE_ID, AUTHORIZED_VIEW_ID); + com.google.bigtable.admin.v2.AuthorizedView proto = + com.google.bigtable.admin.v2.AuthorizedView.newBuilder() + .setName(authorizedViewName.toString()) + .setDeletionProtection(true) + .setSubsetView( + com.google.bigtable.admin.v2.AuthorizedView.SubsetView.newBuilder().build()) + .build(); + AuthorizedView authorizedView = AuthorizedView.fromProto(proto); + + assertThat(authorizedView.hashCode()).isEqualTo(AuthorizedView.fromProto(proto).hashCode()); + + assertThat(authorizedView.hashCode()) + .isNotEqualTo( + com.google.bigtable.admin.v2.AuthorizedView.newBuilder() + .setName(authorizedViewName.toString()) + .setDeletionProtection(false) + .setSubsetView( + com.google.bigtable.admin.v2.AuthorizedView.SubsetView.newBuilder().build()) + .build() + .hashCode()); + } +} diff --git a/google-cloud-bigtable/src/test/java/com/google/cloud/bigtable/admin/v2/models/CreateAuthorizedViewRequestTest.java b/google-cloud-bigtable/src/test/java/com/google/cloud/bigtable/admin/v2/models/CreateAuthorizedViewRequestTest.java new file mode 100644 index 0000000000..c661509cee --- /dev/null +++ b/google-cloud-bigtable/src/test/java/com/google/cloud/bigtable/admin/v2/models/CreateAuthorizedViewRequestTest.java @@ -0,0 +1,108 @@ +/* + * Copyright 2024 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.admin.v2.models; + +import static com.google.common.truth.Truth.assertThat; + +import com.google.cloud.bigtable.admin.v2.internal.NameUtil; +import com.google.protobuf.ByteString; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + +@RunWith(JUnit4.class) +public class CreateAuthorizedViewRequestTest { + private static final String PROJECT_ID = "my-project"; + private static final String INSTANCE_ID = "my-instance"; + private static final String TABLE_ID = "my-table"; + private static final String AUTHORIZED_VIEW_ID = "my-authorized-view"; + + @Test + public void testToProto() { + CreateAuthorizedViewRequest request = + CreateAuthorizedViewRequest.of(TABLE_ID, AUTHORIZED_VIEW_ID) + .setDeletionProtection(true) + .setAuthorizedViewType( + SubsetView.create() + .addRowPrefix("row#") + .addRowPrefix("another-row#") + .setFamilySubsets( + "family", + FamilySubsets.create() + .addQualifier("column") + .addQualifierPrefix("column#"))); + + com.google.bigtable.admin.v2.CreateAuthorizedViewRequest requestProto = + com.google.bigtable.admin.v2.CreateAuthorizedViewRequest.newBuilder() + .setParent(NameUtil.formatTableName(PROJECT_ID, INSTANCE_ID, TABLE_ID)) + .setAuthorizedViewId(AUTHORIZED_VIEW_ID) + .setAuthorizedView( + com.google.bigtable.admin.v2.AuthorizedView.newBuilder() + .setDeletionProtection(true) + .setSubsetView( + com.google.bigtable.admin.v2.AuthorizedView.SubsetView.newBuilder() + .addRowPrefixes(ByteString.copyFromUtf8("row#")) + .addRowPrefixes(ByteString.copyFromUtf8("another-row#")) + .putFamilySubsets( + "family", + com.google.bigtable.admin.v2.AuthorizedView.FamilySubsets + .newBuilder() + .addQualifiers(ByteString.copyFromUtf8("column")) + .addQualifierPrefixes(ByteString.copyFromUtf8("column#")) + .build()))) + .build(); + assertThat(request.toProto(PROJECT_ID, INSTANCE_ID)).isEqualTo(requestProto); + } + + @Test + public void testEquality() { + CreateAuthorizedViewRequest request = + CreateAuthorizedViewRequest.of(TABLE_ID, AUTHORIZED_VIEW_ID) + .setAuthorizedViewType(SubsetView.create().addRowPrefix("row#")) + .setDeletionProtection(false); + + assertThat(request) + .isEqualTo( + CreateAuthorizedViewRequest.of(TABLE_ID, AUTHORIZED_VIEW_ID) + .setAuthorizedViewType(SubsetView.create().addRowPrefix("row#"))); + + assertThat(request) + .isNotEqualTo( + CreateAuthorizedViewRequest.of(TABLE_ID, AUTHORIZED_VIEW_ID) + .setAuthorizedViewType(SubsetView.create().addRowPrefix("another-row#"))); + } + + @Test + public void testHashCode() { + CreateAuthorizedViewRequest request = + CreateAuthorizedViewRequest.of(TABLE_ID, AUTHORIZED_VIEW_ID) + .setAuthorizedViewType(SubsetView.create().addRowPrefix("row#")) + .setDeletionProtection(false); + + assertThat(request.hashCode()) + .isEqualTo( + CreateAuthorizedViewRequest.of(TABLE_ID, AUTHORIZED_VIEW_ID) + .setAuthorizedViewType(SubsetView.create().addRowPrefix("row#")) + .hashCode()); + + assertThat(request.hashCode()) + .isNotEqualTo( + CreateAuthorizedViewRequest.of(TABLE_ID, AUTHORIZED_VIEW_ID) + .setAuthorizedViewType(SubsetView.create().addRowPrefix("another-row#")) + .hashCode()); + } +} diff --git a/google-cloud-bigtable/src/test/java/com/google/cloud/bigtable/admin/v2/models/FamilySubsetsTest.java b/google-cloud-bigtable/src/test/java/com/google/cloud/bigtable/admin/v2/models/FamilySubsetsTest.java new file mode 100644 index 0000000000..bfcc62fe9c --- /dev/null +++ b/google-cloud-bigtable/src/test/java/com/google/cloud/bigtable/admin/v2/models/FamilySubsetsTest.java @@ -0,0 +1,82 @@ +/* + * Copyright 2024 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.admin.v2.models; + +import static com.google.common.truth.Truth.assertThat; + +import com.google.bigtable.admin.v2.AuthorizedView; +import com.google.protobuf.ByteString; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + +@RunWith(JUnit4.class) +public class FamilySubsetsTest { + + @Test + public void testFromProto() { + AuthorizedView.FamilySubsets familySubsetsProto = + com.google.bigtable.admin.v2.AuthorizedView.FamilySubsets.newBuilder() + .addQualifiers(ByteString.copyFromUtf8("column1")) + .addQualifiers(ByteString.copyFromUtf8("column2")) + .addQualifierPrefixes(ByteString.copyFromUtf8("column3#")) + .addQualifierPrefixes(ByteString.copyFromUtf8("column4#")) + .build(); + + FamilySubsets result = FamilySubsets.fromProto(familySubsetsProto); + + assertThat(result.getQualifiers()) + .containsExactly(ByteString.copyFromUtf8("column1"), ByteString.copyFromUtf8("column2")); + assertThat(result.getQualifierPrefixes()) + .containsExactly(ByteString.copyFromUtf8("column3#"), ByteString.copyFromUtf8("column4#")); + } + + @Test + public void testEquality() { + AuthorizedView.FamilySubsets proto = + com.google.bigtable.admin.v2.AuthorizedView.FamilySubsets.newBuilder() + .addQualifiers(ByteString.copyFromUtf8("column1")) + .build(); + FamilySubsets familySubsets = FamilySubsets.fromProto(proto); + + assertThat(familySubsets).isEqualTo(FamilySubsets.fromProto(proto)); + assertThat(familySubsets) + .isNotEqualTo( + FamilySubsets.fromProto( + com.google.bigtable.admin.v2.AuthorizedView.FamilySubsets.newBuilder() + .addQualifierPrefixes(ByteString.copyFromUtf8("column1")) + .build())); + } + + @Test + public void testHashCode() { + AuthorizedView.FamilySubsets proto = + com.google.bigtable.admin.v2.AuthorizedView.FamilySubsets.newBuilder() + .addQualifiers(ByteString.copyFromUtf8("column1")) + .build(); + FamilySubsets familySubsets = FamilySubsets.fromProto(proto); + + assertThat(familySubsets.hashCode()).isEqualTo(FamilySubsets.fromProto(proto).hashCode()); + assertThat(familySubsets.hashCode()) + .isNotEqualTo( + FamilySubsets.fromProto( + com.google.bigtable.admin.v2.AuthorizedView.FamilySubsets.newBuilder() + .addQualifierPrefixes(ByteString.copyFromUtf8("column1")) + .build()) + .hashCode()); + } +} diff --git a/google-cloud-bigtable/src/test/java/com/google/cloud/bigtable/admin/v2/models/SubsetViewTest.java b/google-cloud-bigtable/src/test/java/com/google/cloud/bigtable/admin/v2/models/SubsetViewTest.java new file mode 100644 index 0000000000..9f5ed078ee --- /dev/null +++ b/google-cloud-bigtable/src/test/java/com/google/cloud/bigtable/admin/v2/models/SubsetViewTest.java @@ -0,0 +1,162 @@ +/* + * Copyright 2024 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.admin.v2.models; + +import static com.google.common.truth.Truth.assertThat; + +import com.google.protobuf.ByteString; +import java.util.Map; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + +@RunWith(JUnit4.class) +public class SubsetViewTest { + + @Test + public void testFromProto() { + com.google.bigtable.admin.v2.AuthorizedView.SubsetView subsetViewProto = + com.google.bigtable.admin.v2.AuthorizedView.SubsetView.newBuilder() + .addRowPrefixes(ByteString.copyFromUtf8("row1#")) + .addRowPrefixes(ByteString.copyFromUtf8("row2#")) + .putFamilySubsets( + "family1", + com.google.bigtable.admin.v2.AuthorizedView.FamilySubsets.newBuilder() + .addQualifiers(ByteString.copyFromUtf8("column1")) + .addQualifiers(ByteString.copyFromUtf8("column2")) + .addQualifierPrefixes(ByteString.copyFromUtf8("column3#")) + .addQualifierPrefixes(ByteString.copyFromUtf8("column4#")) + .build()) + .putFamilySubsets( + "family2", + com.google.bigtable.admin.v2.AuthorizedView.FamilySubsets.newBuilder() + .addQualifiers(ByteString.copyFromUtf8("column5")) + .addQualifierPrefixes(ByteString.copyFromUtf8("")) + .build()) + .build(); + + SubsetView result = SubsetView.fromProto(subsetViewProto); + + assertThat(result.getRowPrefixes()) + .containsExactly(ByteString.copyFromUtf8("row1#"), ByteString.copyFromUtf8("row2#")); + + Map familySubsetsResult = result.getFamilySubsets(); + assertThat(familySubsetsResult) + .containsExactly( + "family1", + FamilySubsets.fromProto(subsetViewProto.getFamilySubsetsOrThrow("family1")), + "family2", + FamilySubsets.fromProto(subsetViewProto.getFamilySubsetsOrThrow("family2"))); + assertThat(familySubsetsResult.get("family1").getQualifiers()) + .containsExactly(ByteString.copyFromUtf8("column1"), ByteString.copyFromUtf8("column2")); + assertThat(familySubsetsResult.get("family1").getQualifierPrefixes()) + .containsExactly(ByteString.copyFromUtf8("column3#"), ByteString.copyFromUtf8("column4#")); + assertThat(familySubsetsResult.get("family2").getQualifiers()) + .containsExactly(ByteString.copyFromUtf8("column5")); + assertThat(familySubsetsResult.get("family2").getQualifierPrefixes()) + .containsExactly(ByteString.copyFromUtf8("")); + } + + @Test + public void testToProto() { + SubsetView subsetView = + SubsetView.create() + .addRowPrefix("row1#") + .addRowPrefix("row2#") + .setFamilySubsets( + "family1", + FamilySubsets.create().addQualifier("column1").addQualifierPrefix("prefix1#")) + .setFamilySubsets( + "family1", + FamilySubsets.create().addQualifier("column2").addQualifierPrefix("prefix2#")) + .setFamilySubsets( + "family2", FamilySubsets.create().addQualifier("column").addQualifierPrefix("")); + + com.google.bigtable.admin.v2.AuthorizedView.SubsetView subsetViewProto = + com.google.bigtable.admin.v2.AuthorizedView.SubsetView.newBuilder() + .addRowPrefixes(ByteString.copyFromUtf8("row1#")) + .addRowPrefixes(ByteString.copyFromUtf8("row2#")) + .putFamilySubsets( + "family1", + com.google.bigtable.admin.v2.AuthorizedView.FamilySubsets.newBuilder() + .addQualifiers(ByteString.copyFromUtf8("column2")) + .addQualifierPrefixes(ByteString.copyFromUtf8("prefix2#")) + .build()) + .putFamilySubsets( + "family2", + com.google.bigtable.admin.v2.AuthorizedView.FamilySubsets.newBuilder() + .addQualifiers(ByteString.copyFromUtf8("column")) + .addQualifierPrefixes(ByteString.copyFromUtf8("")) + .build()) + .build(); + + assertThat(subsetView.getRowPrefixes()) + .containsExactly(ByteString.copyFromUtf8("row1#"), ByteString.copyFromUtf8("row2#")); + Map familySubsetsResult = subsetView.getFamilySubsets(); + assertThat(familySubsetsResult) + .containsExactly( + "family1", + FamilySubsets.fromProto(subsetViewProto.getFamilySubsetsOrThrow("family1")), + "family2", + FamilySubsets.fromProto(subsetViewProto.getFamilySubsetsOrThrow("family2"))); + assertThat(familySubsetsResult.get("family1").getQualifiers()) + .containsExactly(ByteString.copyFromUtf8("column2")); + assertThat(familySubsetsResult.get("family1").getQualifierPrefixes()) + .containsExactly(ByteString.copyFromUtf8("prefix2#")); + assertThat(familySubsetsResult.get("family2").getQualifiers()) + .containsExactly(ByteString.copyFromUtf8("column")); + assertThat(familySubsetsResult.get("family2").getQualifierPrefixes()) + .containsExactly(ByteString.copyFromUtf8("")); + + assertThat(subsetView.toProto()).isEqualTo(subsetViewProto); + } + + @Test + public void testEquality() { + com.google.bigtable.admin.v2.AuthorizedView.SubsetView proto = + com.google.bigtable.admin.v2.AuthorizedView.SubsetView.newBuilder() + .addRowPrefixes(ByteString.copyFromUtf8("row1#")) + .build(); + SubsetView subsetView = SubsetView.fromProto(proto); + + assertThat(subsetView).isEqualTo(SubsetView.fromProto(proto)); + assertThat(subsetView) + .isNotEqualTo( + SubsetView.fromProto( + com.google.bigtable.admin.v2.AuthorizedView.SubsetView.newBuilder() + .addRowPrefixes(ByteString.copyFromUtf8("row2#")) + .build())); + } + + @Test + public void testHashCode() { + com.google.bigtable.admin.v2.AuthorizedView.SubsetView proto = + com.google.bigtable.admin.v2.AuthorizedView.SubsetView.newBuilder() + .addRowPrefixes(ByteString.copyFromUtf8("row1#")) + .build(); + SubsetView subsetView = SubsetView.fromProto(proto); + + assertThat(subsetView.hashCode()).isEqualTo(SubsetView.fromProto(proto).hashCode()); + assertThat(subsetView.hashCode()) + .isNotEqualTo( + SubsetView.fromProto( + com.google.bigtable.admin.v2.AuthorizedView.SubsetView.newBuilder() + .addRowPrefixes(ByteString.copyFromUtf8("row2#")) + .build()) + .hashCode()); + } +} diff --git a/google-cloud-bigtable/src/test/java/com/google/cloud/bigtable/admin/v2/models/UpdateAuthorizedViewRequestTest.java b/google-cloud-bigtable/src/test/java/com/google/cloud/bigtable/admin/v2/models/UpdateAuthorizedViewRequestTest.java new file mode 100644 index 0000000000..06c9a8353e --- /dev/null +++ b/google-cloud-bigtable/src/test/java/com/google/cloud/bigtable/admin/v2/models/UpdateAuthorizedViewRequestTest.java @@ -0,0 +1,158 @@ +/* + * Copyright 2024 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.admin.v2.models; + +import static com.google.common.truth.Truth.assertThat; + +import com.google.cloud.bigtable.admin.v2.internal.NameUtil; +import com.google.protobuf.ByteString; +import com.google.protobuf.FieldMask; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + +@RunWith(JUnit4.class) +public class UpdateAuthorizedViewRequestTest { + private static final String PROJECT_ID = "my-project"; + private static final String INSTANCE_ID = "my-instance"; + private static final String TABLE_ID = "my-table"; + private static final String AUTHORIZED_VIEW_ID = "my-authorized-view"; + + @Test + public void testToProto() { + UpdateAuthorizedViewRequest request = + UpdateAuthorizedViewRequest.of(TABLE_ID, AUTHORIZED_VIEW_ID) + .setDeletionProtection(true) + .setAuthorizedViewType(SubsetView.create().addRowPrefix("row#")) + .setIgnoreWarnings(true); + + com.google.bigtable.admin.v2.UpdateAuthorizedViewRequest requestProto = + com.google.bigtable.admin.v2.UpdateAuthorizedViewRequest.newBuilder() + .setAuthorizedView( + com.google.bigtable.admin.v2.AuthorizedView.newBuilder() + .setName( + NameUtil.formatAuthorizedViewName( + PROJECT_ID, INSTANCE_ID, TABLE_ID, AUTHORIZED_VIEW_ID)) + .setDeletionProtection(true) + .setSubsetView( + com.google.bigtable.admin.v2.AuthorizedView.SubsetView.newBuilder() + .addRowPrefixes(ByteString.copyFromUtf8("row#")))) + .setUpdateMask( + FieldMask.newBuilder().addPaths("deletion_protection").addPaths("subset_view")) + .setIgnoreWarnings(true) + .build(); + assertThat(request.toProto(PROJECT_ID, INSTANCE_ID)).isEqualTo(requestProto); + } + + @Test + public void testUpdateDeletionProtection() { + com.google.bigtable.admin.v2.AuthorizedView existingAuthorizedView = + com.google.bigtable.admin.v2.AuthorizedView.newBuilder() + .setName( + NameUtil.formatAuthorizedViewName( + PROJECT_ID, INSTANCE_ID, TABLE_ID, AUTHORIZED_VIEW_ID)) + .setDeletionProtection(true) + .setSubsetView( + com.google.bigtable.admin.v2.AuthorizedView.SubsetView.newBuilder() + .addRowPrefixes(ByteString.copyFromUtf8("row#"))) + .build(); + + UpdateAuthorizedViewRequest request = + UpdateAuthorizedViewRequest.of(AuthorizedView.fromProto(existingAuthorizedView)) + .setDeletionProtection(false); + + com.google.bigtable.admin.v2.UpdateAuthorizedViewRequest requestProto = + com.google.bigtable.admin.v2.UpdateAuthorizedViewRequest.newBuilder() + .setAuthorizedView(existingAuthorizedView.toBuilder().setDeletionProtection(false)) + .setUpdateMask(FieldMask.newBuilder().addPaths("deletion_protection")) + .build(); + assertThat(request.toProto(PROJECT_ID, INSTANCE_ID)).isEqualTo(requestProto); + } + + @Test + public void testUpdateSubsetView() { + com.google.bigtable.admin.v2.AuthorizedView authorizedViewProto = + com.google.bigtable.admin.v2.AuthorizedView.newBuilder() + .setName( + NameUtil.formatAuthorizedViewName( + PROJECT_ID, INSTANCE_ID, TABLE_ID, AUTHORIZED_VIEW_ID)) + .setSubsetView( + com.google.bigtable.admin.v2.AuthorizedView.SubsetView.newBuilder() + .addRowPrefixes(ByteString.copyFromUtf8("row#")) + .putFamilySubsets( + "cf", + com.google.bigtable.admin.v2.AuthorizedView.FamilySubsets.newBuilder() + .addQualifiers(ByteString.copyFromUtf8("qualifier")) + .addQualifierPrefixes(ByteString.copyFromUtf8("prefix#")) + .build())) + .build(); + + UpdateAuthorizedViewRequest request = + UpdateAuthorizedViewRequest.of(TABLE_ID, AUTHORIZED_VIEW_ID) + .setAuthorizedViewType( + SubsetView.create() + .addRowPrefix("row#") + .setFamilySubsets( + "cf", + FamilySubsets.create() + .addQualifier("qualifier") + .addQualifierPrefix("prefix#"))); + + com.google.bigtable.admin.v2.UpdateAuthorizedViewRequest requestProto = + com.google.bigtable.admin.v2.UpdateAuthorizedViewRequest.newBuilder() + .setAuthorizedView(authorizedViewProto) + .setUpdateMask(FieldMask.newBuilder().addPaths("subset_view")) + .build(); + assertThat(request.toProto(PROJECT_ID, INSTANCE_ID)).isEqualTo(requestProto); + } + + @Test + public void testEquality() { + UpdateAuthorizedViewRequest request = + UpdateAuthorizedViewRequest.of(TABLE_ID, AUTHORIZED_VIEW_ID) + .setAuthorizedViewType(SubsetView.create().addRowPrefix("row#")); + + assertThat(request) + .isEqualTo( + UpdateAuthorizedViewRequest.of(TABLE_ID, AUTHORIZED_VIEW_ID) + .setAuthorizedViewType(SubsetView.create().addRowPrefix("row#"))); + + assertThat(request) + .isNotEqualTo( + UpdateAuthorizedViewRequest.of(TABLE_ID, AUTHORIZED_VIEW_ID) + .setAuthorizedViewType(SubsetView.create().addRowPrefix("another-row#"))); + } + + @Test + public void testHashCode() { + UpdateAuthorizedViewRequest request = + UpdateAuthorizedViewRequest.of(TABLE_ID, AUTHORIZED_VIEW_ID) + .setAuthorizedViewType(SubsetView.create().addRowPrefix("row#")); + + assertThat(request.hashCode()) + .isEqualTo( + UpdateAuthorizedViewRequest.of(TABLE_ID, AUTHORIZED_VIEW_ID) + .setAuthorizedViewType(SubsetView.create().addRowPrefix("row#")) + .hashCode()); + + assertThat(request.hashCode()) + .isNotEqualTo( + UpdateAuthorizedViewRequest.of(TABLE_ID, AUTHORIZED_VIEW_ID) + .setAuthorizedViewType(SubsetView.create().addRowPrefix("another-row#")) + .hashCode()); + } +} diff --git a/google-cloud-bigtable/src/test/java/com/google/cloud/bigtable/data/v2/BigtableDataClientTests.java b/google-cloud-bigtable/src/test/java/com/google/cloud/bigtable/data/v2/BigtableDataClientTests.java index f4f23085a2..880744bc18 100644 --- a/google-cloud-bigtable/src/test/java/com/google/cloud/bigtable/data/v2/BigtableDataClientTests.java +++ b/google-cloud-bigtable/src/test/java/com/google/cloud/bigtable/data/v2/BigtableDataClientTests.java @@ -24,6 +24,7 @@ import com.google.api.gax.rpc.ResponseObserver; import com.google.api.gax.rpc.ServerStreamingCallable; import com.google.api.gax.rpc.UnaryCallable; +import com.google.cloud.bigtable.data.v2.models.AuthorizedViewId; import com.google.cloud.bigtable.data.v2.models.BulkMutation; import com.google.cloud.bigtable.data.v2.models.ChangeStreamRecord; import com.google.cloud.bigtable.data.v2.models.ConditionalRowMutation; @@ -38,6 +39,9 @@ import com.google.cloud.bigtable.data.v2.models.RowCell; import com.google.cloud.bigtable.data.v2.models.RowMutation; import com.google.cloud.bigtable.data.v2.models.RowMutationEntry; +import com.google.cloud.bigtable.data.v2.models.SampleRowKeysRequest; +import com.google.cloud.bigtable.data.v2.models.TableId; +import com.google.cloud.bigtable.data.v2.models.TargetId; import com.google.cloud.bigtable.data.v2.stub.EnhancedBigtableStub; import com.google.common.collect.ImmutableList; import com.google.protobuf.ByteString; @@ -75,6 +79,10 @@ public class BigtableDataClientTests { @Mock private UnaryCallable mockReadRowCallable; @Mock private UnaryCallable> mockSampleRowKeysCallable; + + @Mock + private UnaryCallable> mockSampleRowKeysCallableWithRequest; + @Mock private UnaryCallable mockMutateRowCallable; @Mock private UnaryCallable mockCheckAndMutateRowCallable; @Mock private UnaryCallable mockReadModifyWriteRowCallable; @@ -130,6 +138,37 @@ public void existsTest() { Mockito.verify(mockReadRowCallable, Mockito.times(2)).futureCall(expectedQuery); } + @Test + public void existsOnAuthorizedViewTest() { + Mockito.when(mockStub.readRowCallable()).thenReturn(mockReadRowCallable); + + Query expectedQuery = + Query.create(AuthorizedViewId.of("fake-table", "fake-authorized-view")) + .rowKey("fake-row-key") + .filter( + FILTERS + .chain() + .filter(FILTERS.limit().cellsPerRow(1)) + .filter(FILTERS.value().strip())); + Row row = Row.create(ByteString.copyFromUtf8("fake-row-key"), ImmutableList.of()); + Mockito.when(mockReadRowCallable.futureCall(expectedQuery)) + .thenReturn(ApiFutures.immediateFuture(row)) + .thenReturn(ApiFutures.immediateFuture(null)); + + boolean result = + bigtableDataClient.exists( + AuthorizedViewId.of("fake-table", "fake-authorized-view"), "fake-row-key"); + boolean anotherResult = + bigtableDataClient.exists( + AuthorizedViewId.of("fake-table", "fake-authorized-view"), + ByteString.copyFromUtf8("fake-row-key")); + + assertThat(result).isTrue(); + assertThat(anotherResult).isFalse(); + + Mockito.verify(mockReadRowCallable, Mockito.times(2)).futureCall(expectedQuery); + } + @Test public void existsAsyncTest() throws Exception { Mockito.when(mockStub.readRowCallable()).thenReturn(mockReadRowCallable); @@ -158,6 +197,38 @@ public void existsAsyncTest() throws Exception { Mockito.verify(mockReadRowCallable, Mockito.times(2)).futureCall(expectedQuery); } + @Test + public void existsOnAuthorizedViewAsyncTest() throws Exception { + Mockito.when(mockStub.readRowCallable()).thenReturn(mockReadRowCallable); + + Query expectedQuery = + Query.create(AuthorizedViewId.of("fake-table", "fake-authorized-view")) + .rowKey("fake-row-key") + .filter( + FILTERS + .chain() + .filter(FILTERS.limit().cellsPerRow(1)) + .filter(FILTERS.value().strip())); + Row row = Row.create(ByteString.copyFromUtf8("fake-row-key"), ImmutableList.of()); + + Mockito.when(mockReadRowCallable.futureCall(expectedQuery)) + .thenReturn(ApiFutures.immediateFuture(row)) + .thenReturn(ApiFutures.immediateFuture(null)); + + ApiFuture result = + bigtableDataClient.existsAsync( + AuthorizedViewId.of("fake-table", "fake-authorized-view"), + ByteString.copyFromUtf8("fake-row-key")); + assertThat(result.get()).isTrue(); + + ApiFuture anotherResult = + bigtableDataClient.existsAsync( + AuthorizedViewId.of("fake-table", "fake-authorized-view"), "fake-row-key"); + assertThat(anotherResult.get()).isFalse(); + + Mockito.verify(mockReadRowCallable, Mockito.times(2)).futureCall(expectedQuery); + } + @Test public void proxyReadRowsCallableTest() { Mockito.when(mockStub.readRowsCallable()).thenReturn(mockReadRowsCallable); @@ -188,6 +259,19 @@ public void proxyReadRowAsyncTest() { .futureCall(Query.create("fake-table").rowKey("fake-row-key")); } + @Test + public void proxyReadRowOnAuthorizedViewAsyncTest() { + Mockito.when(mockStub.readRowCallable()).thenReturn(mockReadRowCallable); + + bigtableDataClient.readRowAsync( + AuthorizedViewId.of("fake-table", "fake-authorized-view"), + ByteString.copyFromUtf8("fake-row-key")); + Mockito.verify(mockReadRowCallable) + .futureCall( + Query.create(AuthorizedViewId.of("fake-table", "fake-authorized-view")) + .rowKey("fake-row-key")); + } + @Test public void proxyReadRowStrAsyncTest() { Mockito.when(mockStub.readRowCallable()).thenReturn(mockReadRowCallable); @@ -197,6 +281,18 @@ public void proxyReadRowStrAsyncTest() { .futureCall(Query.create("fake-table").rowKey("fake-row-key")); } + @Test + public void proxyReadRowOnAuthorizedViewStrAsyncTest() { + Mockito.when(mockStub.readRowCallable()).thenReturn(mockReadRowCallable); + + bigtableDataClient.readRowAsync( + AuthorizedViewId.of("fake-table", "fake-authorized-view"), "fake-row-key"); + Mockito.verify(mockReadRowCallable) + .futureCall( + Query.create(AuthorizedViewId.of("fake-table", "fake-authorized-view")) + .rowKey("fake-row-key")); + } + @Test public void readRowFilterAsyncTest() { Mockito.when(mockStub.readRowCallable()).thenReturn(mockReadRowCallable); @@ -213,6 +309,28 @@ public void readRowFilterAsyncTest() { .futureCall(Query.create("fake-table").rowKey("fake-row-key").filter(filter)); } + @Test + public void readRowOnAuthorizedViewFilterAsyncTest() { + Mockito.when(mockStub.readRowCallable()).thenReturn(mockReadRowCallable); + + // Build the filter expression + Filter filter = + FILTERS + .chain() + .filter(FILTERS.qualifier().regex("prefix.*")) + .filter(FILTERS.limit().cellsPerRow(10)); + bigtableDataClient.readRowAsync( + AuthorizedViewId.of("fake-table", "fake-authorized-view"), + ByteString.copyFromUtf8("fake-row-key"), + filter); + + Mockito.verify(mockReadRowCallable) + .futureCall( + Query.create(AuthorizedViewId.of("fake-table", "fake-authorized-view")) + .rowKey("fake-row-key") + .filter(filter)); + } + @Test public void readRowFilterStrAsyncTest() { Mockito.when(mockStub.readRowCallable()).thenReturn(mockReadRowCallable); @@ -229,6 +347,26 @@ public void readRowFilterStrAsyncTest() { .futureCall(Query.create("fake-table").rowKey("fake-row-key").filter(filter)); } + @Test + public void readRowOnAuthorizedViewFilterStrAsyncTest() { + Mockito.when(mockStub.readRowCallable()).thenReturn(mockReadRowCallable); + + // Build the filter expression + Filter filter = + FILTERS + .chain() + .filter(FILTERS.qualifier().regex("prefix.*")) + .filter(FILTERS.limit().cellsPerRow(10)); + bigtableDataClient.readRowAsync( + AuthorizedViewId.of("fake-table", "fake-authorized-view"), "fake-row-key", filter); + + Mockito.verify(mockReadRowCallable) + .futureCall( + Query.create(AuthorizedViewId.of("fake-table", "fake-authorized-view")) + .rowKey("fake-row-key") + .filter(filter)); + } + @Test public void readRowTest() { Mockito.when(mockStub.readRowCallable()).thenReturn(mockReadRowCallable); @@ -244,6 +382,26 @@ public void readRowTest() { assertThat(actualRow).isEqualTo(expectedRow); } + @Test + public void readRowOnAuthorizedViewTest() { + Mockito.when(mockStub.readRowCallable()).thenReturn(mockReadRowCallable); + + Row expectedRow = + Row.create(ByteString.copyFromUtf8("fake-row-key"), ImmutableList.of()); + Mockito.when( + mockReadRowCallable.futureCall( + Query.create(AuthorizedViewId.of("fake-table", "fake-authorized-view")) + .rowKey("fake-row-key"))) + .thenReturn(ApiFutures.immediateFuture(expectedRow)); + + Row actualRow = + bigtableDataClient.readRow( + AuthorizedViewId.of("fake-table", "fake-authorized-view"), + ByteString.copyFromUtf8("fake-row-key")); + + assertThat(actualRow).isEqualTo(expectedRow); + } + @Test public void readRowStrTest() { Mockito.when(mockStub.readRowCallable()).thenReturn(mockReadRowCallable); @@ -258,6 +416,25 @@ public void readRowStrTest() { assertThat(actualRow).isEqualTo(expectedRow); } + @Test + public void readRowOnAuthorizedViewStrTest() { + Mockito.when(mockStub.readRowCallable()).thenReturn(mockReadRowCallable); + + Row expectedRow = + Row.create(ByteString.copyFromUtf8("fake-row-key"), ImmutableList.of()); + Mockito.when( + mockReadRowCallable.futureCall( + Query.create(AuthorizedViewId.of("fake-table", "fake-authorized-view")) + .rowKey("fake-row-key"))) + .thenReturn(ApiFutures.immediateFuture(expectedRow)); + + Row actualRow = + bigtableDataClient.readRow( + AuthorizedViewId.of("fake-table", "fake-authorized-view"), "fake-row-key"); + + assertThat(actualRow).isEqualTo(expectedRow); + } + @Test public void readRowFilterTest() { Mockito.when(mockStub.readRowCallable()).thenReturn(mockReadRowCallable); @@ -282,6 +459,35 @@ public void readRowFilterTest() { assertThat(actualRow).isEqualTo(expectedRow); } + @Test + public void readRowOnAuthorizedViewFilterTest() { + Mockito.when(mockStub.readRowCallable()).thenReturn(mockReadRowCallable); + + // Build the filter expression + Filter filter = + FILTERS + .chain() + .filter(FILTERS.qualifier().regex("prefix.*")) + .filter(FILTERS.limit().cellsPerRow(10)); + + Row expectedRow = + Row.create(ByteString.copyFromUtf8("fake-row-key"), ImmutableList.of()); + Mockito.when( + mockReadRowCallable.futureCall( + Query.create(AuthorizedViewId.of("fake-table", "fake-authorized-view")) + .rowKey("fake-row-key") + .filter(filter))) + .thenReturn(ApiFutures.immediateFuture(expectedRow)); + + Row actualRow = + bigtableDataClient.readRow( + AuthorizedViewId.of("fake-table", "fake-authorized-view"), + ByteString.copyFromUtf8("fake-row-key"), + filter); + + assertThat(actualRow).isEqualTo(expectedRow); + } + @Test public void readRowStrFilterTest() { Mockito.when(mockStub.readRowCallable()).thenReturn(mockReadRowCallable); @@ -304,6 +510,32 @@ public void readRowStrFilterTest() { assertThat(actualRow).isEqualTo(expectedRow); } + @Test + public void readRowOnAuthorizedViewStrFilterTest() { + Mockito.when(mockStub.readRowCallable()).thenReturn(mockReadRowCallable); + + // Build the filter expression + Filter filter = + FILTERS + .chain() + .filter(FILTERS.qualifier().regex("prefix.*")) + .filter(FILTERS.limit().cellsPerRow(10)); + Row expectedRow = + Row.create(ByteString.copyFromUtf8("fake-row-key"), ImmutableList.of()); + Mockito.when( + mockReadRowCallable.futureCall( + Query.create(AuthorizedViewId.of("fake-table", "fake-authorized-view")) + .rowKey("fake-row-key") + .filter(filter))) + .thenReturn(ApiFutures.immediateFuture(expectedRow)); + + Row actualRow = + bigtableDataClient.readRow( + AuthorizedViewId.of("fake-table", "fake-authorized-view"), "fake-row-key", filter); + + assertThat(actualRow).isEqualTo(expectedRow); + } + @Test public void proxyReadRowsSyncTest() { Mockito.when(mockStub.readRowsCallable()).thenReturn(mockReadRowsCallable); @@ -314,6 +546,16 @@ public void proxyReadRowsSyncTest() { Mockito.verify(mockReadRowsCallable).call(query); } + @Test + public void proxyReadRowsOnAuthorizedViewSyncTest() { + Mockito.when(mockStub.readRowsCallable()).thenReturn(mockReadRowsCallable); + + Query query = Query.create(AuthorizedViewId.of("fake-table", "fake-authorized-view")); + bigtableDataClient.readRows(query); + + Mockito.verify(mockReadRowsCallable).call(query); + } + @Test public void proxyReadRowsAsyncTest() { Mockito.when(mockStub.readRowsCallable()).thenReturn(mockReadRowsCallable); @@ -326,6 +568,18 @@ public void proxyReadRowsAsyncTest() { Mockito.verify(mockReadRowsCallable).call(query, mockObserver); } + @Test + public void proxyReadRowsOnAuthorizedViewAsyncTest() { + Mockito.when(mockStub.readRowsCallable()).thenReturn(mockReadRowsCallable); + + Query query = Query.create(AuthorizedViewId.of("fake-table", "fake-authorized-view")); + @SuppressWarnings("unchecked") + ResponseObserver mockObserver = Mockito.mock(ResponseObserver.class); + bigtableDataClient.readRowsAsync(query, mockObserver); + + Mockito.verify(mockReadRowsCallable).call(query, mockObserver); + } + @Test public void proxyGenerateInitialChangeStreamPartitionsSyncTest() { Mockito.when(mockStub.generateInitialChangeStreamPartitionsCallable()) @@ -381,20 +635,53 @@ public void proxySampleRowKeysCallableTest() { @Test public void proxySampleRowKeysTest() { - Mockito.when(mockStub.sampleRowKeysCallable()).thenReturn(mockSampleRowKeysCallable); + Mockito.when(mockStub.sampleRowKeysCallableWithRequest()) + .thenReturn(mockSampleRowKeysCallableWithRequest); bigtableDataClient.sampleRowKeysAsync("fake-table"); - Mockito.verify(mockSampleRowKeysCallable).futureCall("fake-table"); + Mockito.verify(mockSampleRowKeysCallableWithRequest) + .futureCall(SampleRowKeysRequest.create(TableId.of("fake-table"))); + } + + @Test + public void proxySampleRowKeysOnAuthorizedViewTest() { + Mockito.when(mockStub.sampleRowKeysCallableWithRequest()) + .thenReturn(mockSampleRowKeysCallableWithRequest); + + bigtableDataClient.sampleRowKeysAsync( + AuthorizedViewId.of("fake-table", "fake-authorized-view")); + Mockito.verify(mockSampleRowKeysCallableWithRequest) + .futureCall( + SampleRowKeysRequest.create(AuthorizedViewId.of("fake-table", "fake-authorized-view"))); } @Test public void sampleRowKeysTest() { - Mockito.when(mockStub.sampleRowKeysCallable()).thenReturn(mockSampleRowKeysCallable); + Mockito.when(mockStub.sampleRowKeysCallableWithRequest()) + .thenReturn(mockSampleRowKeysCallableWithRequest); - Mockito.when(mockSampleRowKeysCallable.futureCall(ArgumentMatchers.any(String.class))) + Mockito.when( + mockSampleRowKeysCallableWithRequest.futureCall( + ArgumentMatchers.any(SampleRowKeysRequest.class))) .thenReturn(ApiFutures.immediateFuture(Collections.emptyList())); bigtableDataClient.sampleRowKeys("fake-table"); - Mockito.verify(mockSampleRowKeysCallable).futureCall("fake-table"); + Mockito.verify(mockSampleRowKeysCallableWithRequest) + .futureCall(SampleRowKeysRequest.create(TableId.of("fake-table"))); + } + + @Test + public void sampleRowKeysOnAuthorizedViewTest() { + Mockito.when(mockStub.sampleRowKeysCallableWithRequest()) + .thenReturn(mockSampleRowKeysCallableWithRequest); + + Mockito.when( + mockSampleRowKeysCallableWithRequest.futureCall( + ArgumentMatchers.any(SampleRowKeysRequest.class))) + .thenReturn(ApiFutures.immediateFuture(Collections.emptyList())); + bigtableDataClient.sampleRowKeys(AuthorizedViewId.of("fake-table", "fake-authorized-view")); + Mockito.verify(mockSampleRowKeysCallableWithRequest) + .futureCall( + SampleRowKeysRequest.create(AuthorizedViewId.of("fake-table", "fake-authorized-view"))); } @Test @@ -416,6 +703,18 @@ public void proxyMutateRowTest() { Mockito.verify(mockMutateRowCallable).futureCall(request); } + @Test + public void proxyMutateRowOnAuthorizedViewTest() { + Mockito.when(mockStub.mutateRowCallable()).thenReturn(mockMutateRowCallable); + + RowMutation request = + RowMutation.create(AuthorizedViewId.of("fake-table", "fake-authorized-view"), "some-key") + .setCell("some-family", "fake-qualifier", "fake-value"); + + bigtableDataClient.mutateRowAsync(request); + Mockito.verify(mockMutateRowCallable).futureCall(request); + } + @Test public void mutateRowTest() { Mockito.when(mockStub.mutateRowCallable()).thenReturn(mockMutateRowCallable); @@ -431,6 +730,21 @@ public void mutateRowTest() { Mockito.verify(mockMutateRowCallable).futureCall(request); } + @Test + public void mutateRowOnAuthorizedViewTest() { + Mockito.when(mockStub.mutateRowCallable()).thenReturn(mockMutateRowCallable); + Mockito.when(mockMutateRowCallable.futureCall(ArgumentMatchers.any(RowMutation.class))) + .thenAnswer( + (Answer) invocationOnMock -> ApiFutures.immediateFuture(Empty.getDefaultInstance())); + + RowMutation request = + RowMutation.create(AuthorizedViewId.of("fake-table", "fake-authorized-view"), "some-key") + .setCell("some-family", "fake-qualifier", "fake-value"); + + bigtableDataClient.mutateRow(request); + Mockito.verify(mockMutateRowCallable).futureCall(request); + } + @Test public void proxyBulkMutatesRowTest() { Mockito.when(mockStub.bulkMutateRowsCallable()).thenReturn(mockBulkMutateRowsCallable); @@ -445,6 +759,20 @@ public void proxyBulkMutatesRowTest() { Mockito.verify(mockBulkMutateRowsCallable).futureCall(request); } + @Test + public void proxyBulkMutatesRowOnAuthorizedViewTest() { + Mockito.when(mockStub.bulkMutateRowsCallable()).thenReturn(mockBulkMutateRowsCallable); + + BulkMutation request = + BulkMutation.create(AuthorizedViewId.of("fake-table", "fake-authorized-view")) + .add( + "fake-key", + Mutation.create().setCell("fake-family", "fake-qualifier", "fake-value")); + + bigtableDataClient.bulkMutateRowsAsync(request); + Mockito.verify(mockBulkMutateRowsCallable).futureCall(request); + } + @Test public void bulkMutatesRowTest() { Mockito.when(mockStub.bulkMutateRowsCallable()).thenReturn(mockBulkMutateRowsCallable); @@ -463,6 +791,24 @@ public void bulkMutatesRowTest() { Mockito.verify(mockBulkMutateRowsCallable).futureCall(request); } + @Test + public void bulkMutatesRowOnAuthorizedViewTest() { + Mockito.when(mockStub.bulkMutateRowsCallable()).thenReturn(mockBulkMutateRowsCallable); + + Mockito.when(mockBulkMutateRowsCallable.futureCall(ArgumentMatchers.any(BulkMutation.class))) + .thenAnswer( + (Answer) invocationOnMock -> ApiFutures.immediateFuture(Empty.getDefaultInstance())); + + BulkMutation request = + BulkMutation.create(AuthorizedViewId.of("fake-table", "fake-authorized-view")) + .add( + "fake-key", + Mutation.create().setCell("fake-family", "fake-qualifier", "fake-value")); + + bigtableDataClient.bulkMutateRows(request); + Mockito.verify(mockBulkMutateRowsCallable).futureCall(request); + } + @Test public void proxyNewBulkMutationBatcherTest() { Mockito.when(mockStub.newMutateRowsBatcher(Mockito.any(String.class), Mockito.any())) @@ -481,6 +827,25 @@ public void proxyNewBulkMutationBatcherTest() { Mockito.verify(mockStub).newMutateRowsBatcher(Mockito.any(String.class), Mockito.any()); } + @Test + public void proxyNewBulkMutationBatcherOnAuthorizedViewTest() { + Mockito.when(mockStub.newMutateRowsBatcher(Mockito.any(TargetId.class), Mockito.any())) + .thenReturn(mockBulkMutationBatcher); + + ApiFuture expectedResponse = ApiFutures.immediateFuture(null); + Batcher batcher = + bigtableDataClient.newBulkMutationBatcher( + AuthorizedViewId.of("fake-table", "fake-authorized-view")); + RowMutationEntry request = + RowMutationEntry.create("some-key").setCell("some-family", "fake-qualifier", "fake-value"); + Mockito.when(mockBulkMutationBatcher.add(request)).thenReturn(expectedResponse); + + ApiFuture actualRes = batcher.add(request); + assertThat(actualRes).isSameInstanceAs(expectedResponse); + + Mockito.verify(mockStub).newMutateRowsBatcher(Mockito.any(TargetId.class), Mockito.any()); + } + @Test public void proxyNewBulkReadRowsTest() { Mockito.when(mockStub.newBulkReadRowsBatcher(Mockito.any(Query.class), Mockito.any())) @@ -500,6 +865,27 @@ public void proxyNewBulkReadRowsTest() { Mockito.verify(mockStub).newBulkReadRowsBatcher(Mockito.any(Query.class), Mockito.any()); } + @Test + public void proxyNewBulkReadRowsOnAuthorizedViewTest() { + Mockito.when(mockStub.newBulkReadRowsBatcher(Mockito.any(Query.class), Mockito.any())) + .thenReturn(mockBulkReadRowsBatcher); + + ApiFuture expectedResponse = + ApiFutures.immediateFuture( + Row.create(ByteString.copyFromUtf8("fake-row-key"), Collections.emptyList())); + ByteString request = ByteString.copyFromUtf8("fake-row-key"); + + Batcher batcher = + bigtableDataClient.newBulkReadRowsBatcher( + AuthorizedViewId.of("fake-table", "fake-authorized-view")); + Mockito.when(mockBulkReadRowsBatcher.add(request)).thenReturn(expectedResponse); + + ApiFuture actualResponse = batcher.add(request); + assertThat(actualResponse).isSameInstanceAs(expectedResponse); + + Mockito.verify(mockStub).newBulkReadRowsBatcher(Mockito.any(Query.class), Mockito.any()); + } + @Test public void proxyNewBulkReadRowsWithFilterTest() { Mockito.when(mockStub.newBulkReadRowsBatcher(Mockito.any(Query.class), Mockito.any())) @@ -520,6 +906,28 @@ public void proxyNewBulkReadRowsWithFilterTest() { Mockito.verify(mockStub).newBulkReadRowsBatcher(Mockito.any(Query.class), Mockito.any()); } + @Test + public void proxyNewBulkReadRowsOnAuthorizedViewWithFilterTest() { + Mockito.when(mockStub.newBulkReadRowsBatcher(Mockito.any(Query.class), Mockito.any())) + .thenReturn(mockBulkReadRowsBatcher); + + ApiFuture expectedResponse = + ApiFutures.immediateFuture( + Row.create(ByteString.copyFromUtf8("fake-row-key"), Collections.emptyList())); + ByteString request = ByteString.copyFromUtf8("fake-row-key"); + + Batcher batcher = + bigtableDataClient.newBulkReadRowsBatcher( + AuthorizedViewId.of("fake-table", "fake-authorized-view"), + FILTERS.key().regex("fake-row")); + Mockito.when(mockBulkReadRowsBatcher.add(request)).thenReturn(expectedResponse); + + ApiFuture actualResponse = batcher.add(request); + assertThat(actualResponse).isSameInstanceAs(expectedResponse); + + Mockito.verify(mockStub).newBulkReadRowsBatcher(Mockito.any(Query.class), Mockito.any()); + } + @Test public void proxyCheckAndMutateRowCallableTest() { assertThat(bigtableDataClient.checkAndMutateRowCallable()) @@ -538,6 +946,19 @@ public void proxyCheckAndMutateRowTest() { Mockito.verify(mockCheckAndMutateRowCallable).futureCall(mutation); } + @Test + public void proxyCheckAndMutateRowOnAuthorizedViewTest() { + Mockito.when(mockStub.checkAndMutateRowCallable()).thenReturn(mockCheckAndMutateRowCallable); + + ConditionalRowMutation mutation = + ConditionalRowMutation.create( + AuthorizedViewId.of("fake-table", "fake-authorized-view"), "fake-key") + .then(Mutation.create().setCell("fake-family", "fake-qualifier", "fake-value")); + bigtableDataClient.checkAndMutateRowAsync(mutation); + + Mockito.verify(mockCheckAndMutateRowCallable).futureCall(mutation); + } + @Test public void checkAndMutateRowTest() { Mockito.when(mockStub.checkAndMutateRowCallable()).thenReturn(mockCheckAndMutateRowCallable); @@ -554,6 +975,23 @@ public void checkAndMutateRowTest() { Mockito.verify(mockCheckAndMutateRowCallable).futureCall(mutation); } + @Test + public void checkAndMutateRowOnAuthorizedViewTest() { + Mockito.when(mockStub.checkAndMutateRowCallable()).thenReturn(mockCheckAndMutateRowCallable); + + Mockito.when( + mockCheckAndMutateRowCallable.futureCall( + ArgumentMatchers.any(ConditionalRowMutation.class))) + .thenReturn(ApiFutures.immediateFuture(Boolean.TRUE)); + ConditionalRowMutation mutation = + ConditionalRowMutation.create( + AuthorizedViewId.of("fake-table", "fake-authorized-view"), "fake-key") + .then(Mutation.create().setCell("fake-family", "fake-qualifier", "fake-value")); + bigtableDataClient.checkAndMutateRow(mutation); + + Mockito.verify(mockCheckAndMutateRowCallable).futureCall(mutation); + } + @Test public void proxyReadModifyWriteRowTest() { Mockito.when(mockStub.readModifyWriteRowCallable()).thenReturn(mockReadModifyWriteRowCallable); @@ -565,6 +1003,18 @@ public void proxyReadModifyWriteRowTest() { Mockito.verify(mockReadModifyWriteRowCallable).futureCall(request); } + @Test + public void proxyReadModifyWriteRowOnAuthorizedViewTest() { + Mockito.when(mockStub.readModifyWriteRowCallable()).thenReturn(mockReadModifyWriteRowCallable); + + ReadModifyWriteRow request = + ReadModifyWriteRow.create( + AuthorizedViewId.of("fake-table", "fake-authorized-view"), "some-key") + .append("fake-family", "fake-qualifier", "suffix"); + bigtableDataClient.readModifyWriteRowAsync(request); + Mockito.verify(mockReadModifyWriteRowCallable).futureCall(request); + } + @Test public void readModifyWriteRowTest() { Mockito.when(mockStub.readModifyWriteRowCallable()).thenReturn(mockReadModifyWriteRowCallable); @@ -583,6 +1033,25 @@ public void readModifyWriteRowTest() { Mockito.verify(mockReadModifyWriteRowCallable).futureCall(request); } + @Test + public void readModifyWriteRowOnAuthorizedViewTest() { + Mockito.when(mockStub.readModifyWriteRowCallable()).thenReturn(mockReadModifyWriteRowCallable); + + Mockito.when( + mockReadModifyWriteRowCallable.futureCall( + ArgumentMatchers.any(ReadModifyWriteRow.class))) + .thenReturn( + ApiFutures.immediateFuture( + Row.create( + ByteString.copyFromUtf8("fake-row-key"), Collections.emptyList()))); + ReadModifyWriteRow request = + ReadModifyWriteRow.create( + AuthorizedViewId.of("fake-table", "fake-authorized-view"), "some-key") + .append("fake-family", "fake-qualifier", "suffix"); + bigtableDataClient.readModifyWriteRow(request); + Mockito.verify(mockReadModifyWriteRowCallable).futureCall(request); + } + @Test public void proxyReadModifyWriterRowCallableTest() { Mockito.when(mockStub.readModifyWriteRowCallable()).thenReturn(mockReadModifyWriteRowCallable); diff --git a/google-cloud-bigtable/src/test/java/com/google/cloud/bigtable/data/v2/it/BulkMutateIT.java b/google-cloud-bigtable/src/test/java/com/google/cloud/bigtable/data/v2/it/BulkMutateIT.java index a09d9415f5..a284f8b7cb 100644 --- a/google-cloud-bigtable/src/test/java/com/google/cloud/bigtable/data/v2/it/BulkMutateIT.java +++ b/google-cloud-bigtable/src/test/java/com/google/cloud/bigtable/data/v2/it/BulkMutateIT.java @@ -15,14 +15,21 @@ */ package com.google.cloud.bigtable.data.v2.it; +import static com.google.cloud.bigtable.misc_utilities.AuthorizedViewTestHelper.AUTHORIZED_VIEW_COLUMN_QUALIFIER; +import static com.google.cloud.bigtable.misc_utilities.AuthorizedViewTestHelper.AUTHORIZED_VIEW_ROW_PREFIX; +import static com.google.cloud.bigtable.misc_utilities.AuthorizedViewTestHelper.createTestAuthorizedView; import static com.google.common.truth.Truth.assertThat; import static com.google.common.truth.TruthJUnit.assume; +import static org.junit.Assert.fail; +import com.google.api.gax.batching.Batcher; import com.google.api.gax.batching.BatcherImpl; import com.google.api.gax.batching.BatchingSettings; import com.google.api.gax.batching.FlowControlEventStats; +import com.google.cloud.bigtable.admin.v2.models.AuthorizedView; import com.google.cloud.bigtable.data.v2.BigtableDataClient; import com.google.cloud.bigtable.data.v2.BigtableDataSettings; +import com.google.cloud.bigtable.data.v2.models.AuthorizedViewId; import com.google.cloud.bigtable.data.v2.models.BulkMutation; import com.google.cloud.bigtable.data.v2.models.Query; import com.google.cloud.bigtable.data.v2.models.Row; @@ -88,6 +95,50 @@ public void test() throws IOException, InterruptedException { } } + @Test(timeout = 60 * 1000) + public void testOnAuthorizedView() throws IOException, InterruptedException { + assume() + .withMessage("AuthorizedView is not supported on Emulator") + .that(testEnvRule.env()) + .isNotInstanceOf(EmulatorEnv.class); + + BigtableDataSettings settings = testEnvRule.env().getDataClientSettings(); + String rowPrefix = AUTHORIZED_VIEW_ROW_PREFIX + UUID.randomUUID(); + // Set target latency really low so it'll trigger adjusting thresholds + BigtableDataSettings.Builder builder = + settings.toBuilder().enableBatchMutationLatencyBasedThrottling(2L); + + AuthorizedView testAuthorizedView = createTestAuthorizedView(testEnvRule); + + try (BigtableDataClient client = BigtableDataClient.create(builder.build()); + Batcher batcher = + client.newBulkMutationBatcher( + AuthorizedViewId.of(testEnvRule.env().getTableId(), testAuthorizedView.getId()))) { + + String familyId = testEnvRule.env().getFamilyId(); + + batcher.add( + RowMutationEntry.create(rowPrefix + "test-key") + .setCell(familyId, AUTHORIZED_VIEW_COLUMN_QUALIFIER, "value")); + batcher.flush(); + + // Query a key to make sure the write succeeded + Row row = + testEnvRule + .env() + .getDataClient() + .readRowsCallable() + .first() + .call(Query.create(testEnvRule.env().getTableId()).rowKey(rowPrefix + "test-key")); + assertThat(row.getCells()).hasSize(1); + } + + testEnvRule + .env() + .getTableAdminClient() + .deleteAuthorizedView(testEnvRule.env().getTableId(), testAuthorizedView.getId()); + } + @Test public void testManyMutations() throws IOException, InterruptedException { // Emulator is very slow and will take a long time for the test to run @@ -135,4 +186,74 @@ public void testManyMutations() throws IOException, InterruptedException { assertThat(row.getCells()).hasSize(100002); } } + + @Test(timeout = 60 * 1000) + public void testManyMutationsOnAuthorizedView() throws IOException, InterruptedException { + assume() + .withMessage("AuthorizedView is not supported on Emulator") + .that(testEnvRule.env()) + .isNotInstanceOf(EmulatorEnv.class); + + AuthorizedView testAuthorizedView = createTestAuthorizedView(testEnvRule); + + BigtableDataSettings settings = testEnvRule.env().getDataClientSettings(); + String rowPrefix = AUTHORIZED_VIEW_ROW_PREFIX + UUID.randomUUID(); + + BatchingSettings batchingSettings = + settings.getStubSettings().bulkMutateRowsSettings().getBatchingSettings(); + + settings + .toBuilder() + .stubSettings() + .bulkMutateRowsSettings() + .setBatchingSettings( + batchingSettings.toBuilder().setDelayThreshold(Duration.ofHours(1)).build()); + try (BigtableDataClient client = BigtableDataClient.create(settings); + Batcher batcher = + client.newBulkMutationBatcher( + AuthorizedViewId.of(testEnvRule.env().getTableId(), testAuthorizedView.getId()))) { + String familyId = testEnvRule.env().getFamilyId(); + for (int i = 0; i < 2; i++) { + String key = rowPrefix + "test-key"; + RowMutationEntry rowMutationEntry = RowMutationEntry.create(key); + // Create mutation entries with many columns. The batcher should flush every time. + for (long j = 0; j < 50001; j++) { + rowMutationEntry.setCell(familyId, AUTHORIZED_VIEW_COLUMN_QUALIFIER + j + i, j); + } + batcher.add(rowMutationEntry); + } + batcher.flush(); + // Query a key to make sure the write succeeded + Row row = + client + .readRowsCallable() + .first() + .call(Query.create(testEnvRule.env().getTableId()).rowKey(rowPrefix + "test-key")); + assertThat(row.getCells()).hasSize(100002); + } + + // We should not be able to mutate rows outside the authorized view + try { + try (BigtableDataClient client = BigtableDataClient.create(settings); + Batcher batcherOutsideAuthorizedView = + client.newBulkMutationBatcher( + AuthorizedViewId.of( + testEnvRule.env().getTableId(), testAuthorizedView.getId()))) { + String keyOutsideAuthorizedView = UUID.randomUUID() + "-outside-authorized-view"; + RowMutationEntry rowMutationEntry = RowMutationEntry.create(keyOutsideAuthorizedView); + rowMutationEntry.setCell( + testEnvRule.env().getFamilyId(), AUTHORIZED_VIEW_COLUMN_QUALIFIER, "test-value"); + batcherOutsideAuthorizedView.add(rowMutationEntry); + batcherOutsideAuthorizedView.flush(); + } + fail("Should not be able to apply bulk mutation on rows outside authorized view"); + } catch (Exception e) { + // Ignore. + } + + testEnvRule + .env() + .getTableAdminClient() + .deleteAuthorizedView(testEnvRule.env().getTableId(), testAuthorizedView.getId()); + } } diff --git a/google-cloud-bigtable/src/test/java/com/google/cloud/bigtable/data/v2/it/BulkReadIT.java b/google-cloud-bigtable/src/test/java/com/google/cloud/bigtable/data/v2/it/BulkReadIT.java index 99c14ccc4f..5b72328240 100644 --- a/google-cloud-bigtable/src/test/java/com/google/cloud/bigtable/data/v2/it/BulkReadIT.java +++ b/google-cloud-bigtable/src/test/java/com/google/cloud/bigtable/data/v2/it/BulkReadIT.java @@ -15,16 +15,21 @@ */ package com.google.cloud.bigtable.data.v2.it; +import static com.google.cloud.bigtable.misc_utilities.AuthorizedViewTestHelper.createTestAuthorizedView; import static com.google.common.truth.Truth.assertThat; +import static com.google.common.truth.TruthJUnit.assume; import com.google.api.core.ApiFuture; import com.google.api.core.ApiFutures; import com.google.api.gax.batching.Batcher; +import com.google.cloud.bigtable.admin.v2.models.AuthorizedView; import com.google.cloud.bigtable.data.v2.BigtableDataClient; +import com.google.cloud.bigtable.data.v2.models.AuthorizedViewId; import com.google.cloud.bigtable.data.v2.models.BulkMutation; import com.google.cloud.bigtable.data.v2.models.Row; import com.google.cloud.bigtable.data.v2.models.RowCell; import com.google.cloud.bigtable.data.v2.models.RowMutationEntry; +import com.google.cloud.bigtable.test_helpers.env.EmulatorEnv; import com.google.cloud.bigtable.test_helpers.env.TestEnvRule; import com.google.common.collect.ImmutableList; import com.google.protobuf.ByteString; @@ -41,6 +46,8 @@ public class BulkReadIT { @ClassRule public static TestEnvRule testEnvRule = new TestEnvRule(); + private static String AUTHORIZED_VIEW_ROW_PREFIX = "row#"; + private static String AUTHORIZED_VIEW_COLUMN_QUALIFIER = "qualifier"; @Test public void testBulkRead() throws InterruptedException, ExecutionException { @@ -102,4 +109,87 @@ public void testBulkRead() throws InterruptedException, ExecutionException { assertThat(actualRows.get(2)).isEqualTo(expectedRows.get(0)); } } + + @Test + public void testBulkReadOnAuthorizedView() throws InterruptedException, ExecutionException { + assume() + .withMessage("AuthorizedView is not supported on Emulator") + .that(testEnvRule.env()) + .isNotInstanceOf(EmulatorEnv.class); + + AuthorizedView testAuthorizedView = createTestAuthorizedView(testEnvRule); + + BigtableDataClient client = testEnvRule.env().getDataClient(); + String family = testEnvRule.env().getFamilyId(); + String rowPrefix = AUTHORIZED_VIEW_ROW_PREFIX + UUID.randomUUID(); + int numRows = 10; + + BulkMutation bulkMutation = BulkMutation.create(testEnvRule.env().getTableId()); + List expectedRows = new ArrayList<>(); + + for (int i = 0; i < numRows; i++) { + bulkMutation.add( + RowMutationEntry.create(rowPrefix + "-" + i) + .setCell(family, AUTHORIZED_VIEW_COLUMN_QUALIFIER, 10_000L, "value-" + i)); + expectedRows.add( + Row.create( + ByteString.copyFromUtf8(rowPrefix + "-" + i), + ImmutableList.of( + RowCell.create( + family, + ByteString.copyFromUtf8(AUTHORIZED_VIEW_COLUMN_QUALIFIER), + 10_000L, + ImmutableList.of(), + ByteString.copyFromUtf8("value-" + i))))); + } + // Add a row outside the authorized view. + String rowPrefixForRowOutsideAuthorizedView = rowPrefix + numRows; + bulkMutation.add( + RowMutationEntry.create(rowPrefixForRowOutsideAuthorizedView) + .setCell(family, "outside-authorized-view", 10_000L, "test-value")); + client.bulkMutateRows(bulkMutation); + + try (Batcher batcher = + client.newBulkReadRowsBatcher( + AuthorizedViewId.of(testEnvRule.env().getTableId(), testAuthorizedView.getId()))) { + + List> rowFutures = new ArrayList<>(numRows); + + for (int rowCount = 0; rowCount < numRows; rowCount++) { + ApiFuture entryResponse = + batcher.add(ByteString.copyFromUtf8(rowPrefix + "-" + rowCount)); + + rowFutures.add(entryResponse); + } + + batcher.flush(); + List actualRows = ApiFutures.allAsList(rowFutures).get(); + assertThat(actualRows).isEqualTo(expectedRows); + + // To verify non-existent and duplicate row keys + rowFutures = new ArrayList<>(); + + // non-existent row key + rowFutures.add(batcher.add(ByteString.copyFromUtf8(UUID.randomUUID().toString()))); + + // duplicate row key + rowFutures.add(batcher.add(ByteString.copyFromUtf8(rowPrefix + "-" + 0))); + rowFutures.add(batcher.add(ByteString.copyFromUtf8(rowPrefix + "-" + 0))); + + // row key outside authorized view + rowFutures.add(batcher.add(ByteString.copyFromUtf8(rowPrefixForRowOutsideAuthorizedView))); + + batcher.flush(); + actualRows = ApiFutures.allAsList(rowFutures).get(); + assertThat(actualRows.get(0)).isNull(); + assertThat(actualRows.get(1)).isEqualTo(expectedRows.get(0)); + assertThat(actualRows.get(2)).isEqualTo(expectedRows.get(0)); + assertThat(actualRows.get(3)).isNull(); + } + + testEnvRule + .env() + .getTableAdminClient() + .deleteAuthorizedView(testEnvRule.env().getTableId(), testAuthorizedView.getId()); + } } diff --git a/google-cloud-bigtable/src/test/java/com/google/cloud/bigtable/data/v2/it/CheckAndMutateIT.java b/google-cloud-bigtable/src/test/java/com/google/cloud/bigtable/data/v2/it/CheckAndMutateIT.java index 5f53284690..41def01ba6 100644 --- a/google-cloud-bigtable/src/test/java/com/google/cloud/bigtable/data/v2/it/CheckAndMutateIT.java +++ b/google-cloud-bigtable/src/test/java/com/google/cloud/bigtable/data/v2/it/CheckAndMutateIT.java @@ -16,13 +16,23 @@ package com.google.cloud.bigtable.data.v2.it; import static com.google.cloud.bigtable.data.v2.models.Filters.FILTERS; +import static com.google.cloud.bigtable.misc_utilities.AuthorizedViewTestHelper.AUTHORIZED_VIEW_COLUMN_QUALIFIER; +import static com.google.cloud.bigtable.misc_utilities.AuthorizedViewTestHelper.AUTHORIZED_VIEW_ROW_PREFIX; +import static com.google.cloud.bigtable.misc_utilities.AuthorizedViewTestHelper.createTestAuthorizedView; import static com.google.common.truth.Truth.assertThat; +import static com.google.common.truth.TruthJUnit.assume; +import static org.junit.Assert.fail; +import com.google.api.gax.rpc.PermissionDeniedException; +import com.google.cloud.bigtable.admin.v2.models.AuthorizedView; +import com.google.cloud.bigtable.data.v2.BigtableDataClient; +import com.google.cloud.bigtable.data.v2.models.AuthorizedViewId; import com.google.cloud.bigtable.data.v2.models.ConditionalRowMutation; import com.google.cloud.bigtable.data.v2.models.Mutation; import com.google.cloud.bigtable.data.v2.models.Query; import com.google.cloud.bigtable.data.v2.models.Row; import com.google.cloud.bigtable.data.v2.models.RowMutation; +import com.google.cloud.bigtable.test_helpers.env.EmulatorEnv; import com.google.cloud.bigtable.test_helpers.env.TestEnvRule; import com.google.protobuf.ByteString; import java.util.UUID; @@ -71,4 +81,81 @@ public void test() throws Exception { assertThat(row.getCells()).hasSize(3); assertThat(row.getCells().get(2).getValue()).isEqualTo(ByteString.copyFromUtf8("q1")); } + + @Test + public void testOnAuthorizedView() throws Exception { + assume() + .withMessage("AuthorizedView is not supported on Emulator") + .that(testEnvRule.env()) + .isNotInstanceOf(EmulatorEnv.class); + + AuthorizedView testAuthorizedView = createTestAuthorizedView(testEnvRule); + + String tableId = testEnvRule.env().getTableId(); + String familyId = testEnvRule.env().getFamilyId(); + String rowKey = AUTHORIZED_VIEW_ROW_PREFIX + UUID.randomUUID(); + BigtableDataClient dataClient = testEnvRule.env().getDataClient(); + + dataClient + .mutateRowCallable() + .call( + RowMutation.create(AuthorizedViewId.of(tableId, testAuthorizedView.getId()), rowKey) + .setCell(familyId, AUTHORIZED_VIEW_COLUMN_QUALIFIER + "1", "val1") + .setCell(familyId, AUTHORIZED_VIEW_COLUMN_QUALIFIER + "2", "val2")); + + dataClient + .checkAndMutateRowAsync( + ConditionalRowMutation.create( + AuthorizedViewId.of(tableId, testAuthorizedView.getId()), rowKey) + .condition(FILTERS.qualifier().exactMatch(AUTHORIZED_VIEW_COLUMN_QUALIFIER + "1")) + .then( + Mutation.create() + .setCell(familyId, AUTHORIZED_VIEW_COLUMN_QUALIFIER + "3", "q1"))) + .get(1, TimeUnit.MINUTES); + + Row row = dataClient.readRowsCallable().first().call(Query.create(tableId).rowKey(rowKey)); + + assertThat(row.getCells()).hasSize(3); + assertThat(row.getCells().get(2).getValue()).isEqualTo(ByteString.copyFromUtf8("q1")); + + // Conditional mutation for rows exist in the table but outside the authorized view + String rowKeyOutsideAuthorizedView = UUID.randomUUID() + "-outside-authorized-view"; + dataClient + .mutateRowCallable() + .call( + RowMutation.create(tableId, rowKeyOutsideAuthorizedView) + .setCell(familyId, AUTHORIZED_VIEW_COLUMN_QUALIFIER, "value")); + try { + dataClient + .checkAndMutateRowAsync( + ConditionalRowMutation.create( + AuthorizedViewId.of(tableId, testAuthorizedView.getId()), + rowKeyOutsideAuthorizedView) + .condition(FILTERS.qualifier().exactMatch(AUTHORIZED_VIEW_COLUMN_QUALIFIER)) + .then(Mutation.create().setCell(familyId, "new_qualifier", "new-value"))) + .get(1, TimeUnit.MINUTES); + fail("Should not be able to conditional mutate row outside authorized view"); + } catch (Exception e) { + assertThat(e.getCause()).isInstanceOf(PermissionDeniedException.class); + } + + // Column qualifier outside the authorized view + try { + dataClient + .checkAndMutateRowAsync( + ConditionalRowMutation.create( + AuthorizedViewId.of(tableId, testAuthorizedView.getId()), rowKey) + .condition(FILTERS.qualifier().exactMatch(AUTHORIZED_VIEW_COLUMN_QUALIFIER)) + .then(Mutation.create().setCell(familyId, "new_qualifier", "new-value"))) + .get(1, TimeUnit.MINUTES); + fail("Should not be able to perform mutations with cells outside the authorized view"); + } catch (Exception e) { + assertThat(e.getCause()).isInstanceOf(PermissionDeniedException.class); + } + + testEnvRule + .env() + .getTableAdminClient() + .deleteAuthorizedView(testEnvRule.env().getTableId(), testAuthorizedView.getId()); + } } diff --git a/google-cloud-bigtable/src/test/java/com/google/cloud/bigtable/data/v2/it/MutateRowIT.java b/google-cloud-bigtable/src/test/java/com/google/cloud/bigtable/data/v2/it/MutateRowIT.java index 2774cbc648..c99000be48 100644 --- a/google-cloud-bigtable/src/test/java/com/google/cloud/bigtable/data/v2/it/MutateRowIT.java +++ b/google-cloud-bigtable/src/test/java/com/google/cloud/bigtable/data/v2/it/MutateRowIT.java @@ -15,11 +15,20 @@ */ package com.google.cloud.bigtable.data.v2.it; +import static com.google.cloud.bigtable.misc_utilities.AuthorizedViewTestHelper.AUTHORIZED_VIEW_COLUMN_QUALIFIER; +import static com.google.cloud.bigtable.misc_utilities.AuthorizedViewTestHelper.AUTHORIZED_VIEW_ROW_PREFIX; +import static com.google.cloud.bigtable.misc_utilities.AuthorizedViewTestHelper.createTestAuthorizedView; import static com.google.common.truth.Truth.assertThat; +import static com.google.common.truth.TruthJUnit.assume; +import static org.junit.Assert.fail; +import com.google.api.gax.rpc.PermissionDeniedException; +import com.google.cloud.bigtable.admin.v2.models.AuthorizedView; +import com.google.cloud.bigtable.data.v2.models.AuthorizedViewId; import com.google.cloud.bigtable.data.v2.models.Query; import com.google.cloud.bigtable.data.v2.models.Row; import com.google.cloud.bigtable.data.v2.models.RowMutation; +import com.google.cloud.bigtable.test_helpers.env.EmulatorEnv; import com.google.cloud.bigtable.test_helpers.env.TestEnvRule; import com.google.protobuf.ByteString; import java.util.UUID; @@ -70,4 +79,77 @@ public void test() throws Exception { assertThat(row.getCells().get(2).getValue()) .isEqualTo(ByteString.copyFrom(new byte[] {0, 0, 0, 0, 0x12, 0x34, 0x56, 0x78})); } + + @Test + public void testOnAuthorizedView() throws Exception { + assume() + .withMessage("AuthorizedView is not supported on Emulator") + .that(testEnvRule.env()) + .isNotInstanceOf(EmulatorEnv.class); + + AuthorizedView testAuthorizedView = createTestAuthorizedView(testEnvRule); + + String rowKey = AUTHORIZED_VIEW_ROW_PREFIX + UUID.randomUUID(); + String familyId = testEnvRule.env().getFamilyId(); + + testEnvRule + .env() + .getDataClient() + .mutateRowAsync( + RowMutation.create( + AuthorizedViewId.of(testEnvRule.env().getTableId(), testAuthorizedView.getId()), + rowKey) + .setCell(familyId, AUTHORIZED_VIEW_COLUMN_QUALIFIER, "myVal") + .setCell(familyId, AUTHORIZED_VIEW_COLUMN_QUALIFIER + "2", "myVal2") + .setCell(familyId, AUTHORIZED_VIEW_COLUMN_QUALIFIER + "3", "myVal3") + .setCell(familyId, AUTHORIZED_VIEW_COLUMN_QUALIFIER + "4", 0x12345678)) + .get(1, TimeUnit.MINUTES); + + testEnvRule + .env() + .getDataClient() + .mutateRowAsync( + RowMutation.create( + AuthorizedViewId.of(testEnvRule.env().getTableId(), testAuthorizedView.getId()), + rowKey) + .deleteCells(familyId, AUTHORIZED_VIEW_COLUMN_QUALIFIER + "2")) + .get(1, TimeUnit.MINUTES); + + Row row = + testEnvRule + .env() + .getDataClient() + .readRowsCallable() + .first() + .call(Query.create(testEnvRule.env().getTableId()).rowKey(rowKey)); + + assertThat(row.getCells()).hasSize(3); + assertThat(row.getCells().get(0).getValue()).isEqualTo(ByteString.copyFromUtf8("myVal")); + assertThat(row.getCells().get(1).getValue()).isEqualTo(ByteString.copyFromUtf8("myVal3")); + assertThat(row.getCells().get(2).getValue()) + .isEqualTo(ByteString.copyFrom(new byte[] {0, 0, 0, 0, 0x12, 0x34, 0x56, 0x78})); + + // We should not be able to mutate a row outside the authorized view + try { + String rowKeyOutsideAuthorizedView = UUID.randomUUID() + "-outside-authorized-view"; + testEnvRule + .env() + .getDataClient() + .mutateRowAsync( + RowMutation.create( + AuthorizedViewId.of( + testEnvRule.env().getTableId(), testAuthorizedView.getId()), + rowKeyOutsideAuthorizedView) + .setCell(familyId, AUTHORIZED_VIEW_COLUMN_QUALIFIER, "myVal")) + .get(1, TimeUnit.MINUTES); + fail("Should not be able to mutate row outside authorized view"); + } catch (Exception e) { + assertThat(e.getCause()).isInstanceOf(PermissionDeniedException.class); + } + + testEnvRule + .env() + .getTableAdminClient() + .deleteAuthorizedView(testEnvRule.env().getTableId(), testAuthorizedView.getId()); + } } diff --git a/google-cloud-bigtable/src/test/java/com/google/cloud/bigtable/data/v2/it/ReadIT.java b/google-cloud-bigtable/src/test/java/com/google/cloud/bigtable/data/v2/it/ReadIT.java index 6578dbad24..95ed16817e 100644 --- a/google-cloud-bigtable/src/test/java/com/google/cloud/bigtable/data/v2/it/ReadIT.java +++ b/google-cloud-bigtable/src/test/java/com/google/cloud/bigtable/data/v2/it/ReadIT.java @@ -15,6 +15,9 @@ */ package com.google.cloud.bigtable.data.v2.it; +import static com.google.cloud.bigtable.misc_utilities.AuthorizedViewTestHelper.AUTHORIZED_VIEW_COLUMN_QUALIFIER; +import static com.google.cloud.bigtable.misc_utilities.AuthorizedViewTestHelper.AUTHORIZED_VIEW_ROW_PREFIX; +import static com.google.cloud.bigtable.misc_utilities.AuthorizedViewTestHelper.createTestAuthorizedView; import static com.google.common.truth.Truth.assertThat; import static com.google.common.truth.TruthJUnit.assume; @@ -27,8 +30,10 @@ import com.google.api.gax.grpc.InstantiatingGrpcChannelProvider; import com.google.api.gax.rpc.ResponseObserver; import com.google.api.gax.rpc.StreamController; +import com.google.cloud.bigtable.admin.v2.models.AuthorizedView; import com.google.cloud.bigtable.data.v2.BigtableDataClient; import com.google.cloud.bigtable.data.v2.BigtableDataSettings; +import com.google.cloud.bigtable.data.v2.models.AuthorizedViewId; import com.google.cloud.bigtable.data.v2.models.BulkMutation; import com.google.cloud.bigtable.data.v2.models.Query; import com.google.cloud.bigtable.data.v2.models.Range.ByteStringRange; @@ -97,6 +102,55 @@ public void isRowExists() throws Exception { assertThat(testEnvRule.env().getDataClient().existsAsync(tableId, rowKey).get()).isTrue(); } + @Test + public void isRowExistsOnAuthorizedView() throws Exception { + assume() + .withMessage("AuthorizedView is not supported on Emulator") + .that(testEnvRule.env()) + .isNotInstanceOf(EmulatorEnv.class); + + BigtableDataClient dataClient = testEnvRule.env().getDataClient(); + String tableId = testEnvRule.env().getTableId(); + String rowKey = AUTHORIZED_VIEW_ROW_PREFIX + prefix + "-isRowExistsOnAuthorizedView"; + String rowKeyOutsideAuthorizedView = prefix + "-isRowExistsOnAuthorizedView"; + + AuthorizedView testAuthorizedView = createTestAuthorizedView(testEnvRule); + + dataClient.mutateRow( + RowMutation.create(tableId, rowKey) + .setCell(testEnvRule.env().getFamilyId(), AUTHORIZED_VIEW_COLUMN_QUALIFIER, "value")); + dataClient.mutateRow( + RowMutation.create(tableId, rowKeyOutsideAuthorizedView) + .setCell(testEnvRule.env().getFamilyId(), AUTHORIZED_VIEW_COLUMN_QUALIFIER, "value")); + + assertThat(dataClient.exists(AuthorizedViewId.of(tableId, testAuthorizedView.getId()), rowKey)) + .isTrue(); + assertThat( + dataClient.exists( + AuthorizedViewId.of(tableId, testAuthorizedView.getId()), + rowKeyOutsideAuthorizedView)) + .isFalse(); + + // Async + assertThat( + dataClient + .existsAsync(AuthorizedViewId.of(tableId, testAuthorizedView.getId()), rowKey) + .get()) + .isTrue(); + assertThat( + dataClient + .existsAsync( + AuthorizedViewId.of(tableId, testAuthorizedView.getId()), + rowKeyOutsideAuthorizedView) + .get()) + .isFalse(); + + testEnvRule + .env() + .getTableAdminClient() + .deleteAuthorizedView(tableId, testAuthorizedView.getId()); + } + @Test public void readEmpty() throws Throwable { String uniqueKey = prefix + "-readEmpty"; @@ -114,6 +168,55 @@ public void readEmpty() throws Throwable { assertThat(observer.responses).isEmpty(); } + @Test + public void readEmptyOnAuthorizedView() throws Throwable { + assume() + .withMessage("AuthorizedView is not supported on Emulator") + .that(testEnvRule.env()) + .isNotInstanceOf(EmulatorEnv.class); + + String tableId = testEnvRule.env().getTableId(); + BigtableDataClient dataClient = testEnvRule.env().getDataClient(); + String uniqueKey = AUTHORIZED_VIEW_ROW_PREFIX + prefix + "-readEmptyOnAuthorizedView"; + String uniqueKeyOutsideAuthorizedView = prefix + "-readEmptyOnAuthorizedView"; + + AuthorizedView testAuthorizedView = createTestAuthorizedView(testEnvRule); + + Query query = + Query.create(AuthorizedViewId.of(tableId, testAuthorizedView.getId())).rowKey(uniqueKey); + Query queryOutsideAuthorizedView = + Query.create(AuthorizedViewId.of(tableId, testAuthorizedView.getId())) + .rowKey(uniqueKeyOutsideAuthorizedView); + + // Sync + ArrayList rows = Lists.newArrayList(dataClient.readRows(query)); + assertThat(rows).isEmpty(); + + // Row exists on the table but outside the authorized view + dataClient.mutateRow( + RowMutation.create(tableId, uniqueKeyOutsideAuthorizedView) + .setCell(testEnvRule.env().getFamilyId(), AUTHORIZED_VIEW_COLUMN_QUALIFIER, "value")); + rows = Lists.newArrayList(dataClient.readRows(queryOutsideAuthorizedView)); + assertThat(rows).isEmpty(); + + // Async + AccumulatingObserver observer = new AccumulatingObserver(); + testEnvRule.env().getDataClient().readRowsAsync(query, observer); + observer.awaitCompletion(); + assertThat(observer.responses).isEmpty(); + + // Row exists on the table but outside the authorized view + observer = new AccumulatingObserver(); + testEnvRule.env().getDataClient().readRowsAsync(queryOutsideAuthorizedView, observer); + observer.awaitCompletion(); + assertThat(observer.responses).isEmpty(); + + testEnvRule + .env() + .getTableAdminClient() + .deleteAuthorizedView(tableId, testAuthorizedView.getId()); + } + @Test public void read() throws Throwable { int numRows = 5; @@ -169,6 +272,122 @@ public void read() throws Throwable { assertThat(actualRowFuture.get()).isEqualTo(expectedRows.get(0)); } + @Test + public void readOnAuthorizedView() throws Throwable { + assume() + .withMessage("AuthorizedView is not supported on Emulator") + .that(testEnvRule.env()) + .isNotInstanceOf(EmulatorEnv.class); + + int numRows = 5; + List expectedRows = Lists.newArrayList(); + String uniqueKey = AUTHORIZED_VIEW_ROW_PREFIX + prefix + "-readOnAuthorizedView"; + String uniqueKeyOutsideAuthorizedView = prefix + "-readOnAuthorizedView"; + String tableId = testEnvRule.env().getTableId(); + BigtableDataClient dataClient = testEnvRule.env().getDataClient(); + + AuthorizedView testAuthorizedView = createTestAuthorizedView(testEnvRule); + + long timestampMicros = System.currentTimeMillis() * 1_000; + + for (int i = 0; i < numRows; i++) { + dataClient + .mutateRowCallable() + .call( + RowMutation.create(tableId, uniqueKey + "-" + i) + .setCell( + testEnvRule.env().getFamilyId(), + AUTHORIZED_VIEW_COLUMN_QUALIFIER, + timestampMicros, + "my-value")); + + expectedRows.add( + Row.create( + ByteString.copyFromUtf8(uniqueKey + "-" + i), + ImmutableList.of( + RowCell.create( + testEnvRule.env().getFamilyId(), + ByteString.copyFromUtf8(AUTHORIZED_VIEW_COLUMN_QUALIFIER), + timestampMicros, + ImmutableList.of(), + ByteString.copyFromUtf8("my-value"))))); + } + // Add a few rows that outside the authorized view + for (int i = 0; i < numRows; i++) { + dataClient + .mutateRowCallable() + .call( + RowMutation.create(tableId, uniqueKeyOutsideAuthorizedView + "-" + i) + .setCell( + testEnvRule.env().getFamilyId(), + AUTHORIZED_VIEW_COLUMN_QUALIFIER, + timestampMicros, + "my-value")); + } + + // Sync + Query query = + Query.create(AuthorizedViewId.of(tableId, testAuthorizedView.getId())) + .range(uniqueKey + "-0", uniqueKey + "-" + numRows); + Query queryOutsideAuthorizedView = + Query.create(AuthorizedViewId.of(tableId, testAuthorizedView.getId())) + .range( + uniqueKeyOutsideAuthorizedView + "-0", + uniqueKeyOutsideAuthorizedView + "-" + numRows); + + ArrayList actualResults = Lists.newArrayList(dataClient.readRows(query)); + assertThat(actualResults).containsExactlyElementsIn(expectedRows); + + // rows exist but outside the authorized view + ArrayList results = Lists.newArrayList(dataClient.readRows(queryOutsideAuthorizedView)); + assertThat(results).isEmpty(); + + // Async + AccumulatingObserver observer = new AccumulatingObserver(); + dataClient.readRowsAsync(query, observer); + observer.awaitCompletion(); + assertThat(observer.responses).containsExactlyElementsIn(expectedRows); + + // Rows exist but outside the authorized view + observer = new AccumulatingObserver(); + dataClient.readRowsAsync(queryOutsideAuthorizedView, observer); + observer.awaitCompletion(); + assertThat(observer.responses).isEmpty(); + + // Point Sync + Row actualRow = + dataClient.readRow( + AuthorizedViewId.of(tableId, testAuthorizedView.getId()), expectedRows.get(0).getKey()); + assertThat(actualRow).isEqualTo(expectedRows.get(0)); + + // Row exists but outside the authorized view + assertThat( + dataClient.readRow( + AuthorizedViewId.of(tableId, testAuthorizedView.getId()), + uniqueKeyOutsideAuthorizedView + "-0")) + .isNull(); + + // Point Async + ApiFuture actualRowFuture = + dataClient.readRowAsync( + AuthorizedViewId.of(tableId, testAuthorizedView.getId()), expectedRows.get(0).getKey()); + assertThat(actualRowFuture.get()).isEqualTo(expectedRows.get(0)); + + // Row exists but outside the authorized view + assertThat( + dataClient + .readRowAsync( + AuthorizedViewId.of(tableId, testAuthorizedView.getId()), + uniqueKeyOutsideAuthorizedView + "-0") + .get()) + .isNull(); + + testEnvRule + .env() + .getTableAdminClient() + .deleteAuthorizedView(tableId, testAuthorizedView.getId()); + } + @Test public void rangeQueries() { BigtableDataClient client = testEnvRule.env().getDataClient(); @@ -240,6 +459,101 @@ public void rangeQueries() { .isEmpty(); } + @Test + public void rangeQueriesOnAuthorizedView() { + assume() + .withMessage("AuthorizedView is not supported on Emulator") + .that(testEnvRule.env()) + .isNotInstanceOf(EmulatorEnv.class); + + BigtableDataClient client = testEnvRule.env().getDataClient(); + String tableId = testEnvRule.env().getTableId(); + String familyId = testEnvRule.env().getFamilyId(); + String uniqueKey = AUTHORIZED_VIEW_ROW_PREFIX + prefix + "-rangeQueriesOnAuthorizedView"; + String keyA = uniqueKey + "-" + "a"; + String keyZ = uniqueKey + "-" + "z"; + String keyOutsideAuthorizedView = prefix; + + AuthorizedView testAuthorizedView = createTestAuthorizedView(testEnvRule); + + long timestampMicros = System.currentTimeMillis() * 1_000; + + client.bulkMutateRows( + BulkMutation.create(tableId) + .add( + RowMutationEntry.create(keyA) + .setCell(familyId, AUTHORIZED_VIEW_COLUMN_QUALIFIER, timestampMicros, "A")) + .add( + RowMutationEntry.create(keyZ) + .setCell(familyId, AUTHORIZED_VIEW_COLUMN_QUALIFIER, timestampMicros, "Z")) + .add( + RowMutationEntry.create(keyOutsideAuthorizedView) + .setCell( + familyId, + AUTHORIZED_VIEW_COLUMN_QUALIFIER, + timestampMicros, + "outsideAuthorizedView"))); + + Row expectedRowA = + Row.create( + ByteString.copyFromUtf8(keyA), + ImmutableList.of( + RowCell.create( + testEnvRule.env().getFamilyId(), + ByteString.copyFromUtf8(AUTHORIZED_VIEW_COLUMN_QUALIFIER), + timestampMicros, + ImmutableList.of(), + ByteString.copyFromUtf8("A")))); + + Row expectedRowZ = + Row.create( + ByteString.copyFromUtf8(keyZ), + ImmutableList.of( + RowCell.create( + testEnvRule.env().getFamilyId(), + ByteString.copyFromUtf8(AUTHORIZED_VIEW_COLUMN_QUALIFIER), + timestampMicros, + ImmutableList.of(), + ByteString.copyFromUtf8("Z")))); + + // Closed/Open + assertThat( + ImmutableList.copyOf( + client.readRows( + Query.create(AuthorizedViewId.of(tableId, testAuthorizedView.getId())) + .range(ByteStringRange.unbounded().startClosed(keyA).endOpen(keyZ))))) + .containsExactly(expectedRowA); + + // Closed/Closed + assertThat( + ImmutableList.copyOf( + client.readRows( + Query.create(AuthorizedViewId.of(tableId, testAuthorizedView.getId())) + .range(ByteStringRange.unbounded().startClosed(keyA).endClosed(keyZ))))) + .containsExactly(expectedRowA, expectedRowZ); + + // Open/Closed + assertThat( + ImmutableList.copyOf( + client.readRows( + Query.create(AuthorizedViewId.of(tableId, testAuthorizedView.getId())) + .range(ByteStringRange.unbounded().startOpen(keyA).endClosed(keyZ))))) + .containsExactly(expectedRowZ); + + // Open/Open + assertThat( + ImmutableList.copyOf( + client.readRows( + Query.create(AuthorizedViewId.of(tableId, testAuthorizedView.getId())) + .range(ByteStringRange.unbounded().startOpen(keyA).endOpen(keyZ))))) + .isEmpty(); + + testEnvRule + .env() + .getTableAdminClient() + .deleteAuthorizedView(tableId, testAuthorizedView.getId()); + } + @Test public void reversed() { assume() diff --git a/google-cloud-bigtable/src/test/java/com/google/cloud/bigtable/data/v2/it/ReadModifyWriteIT.java b/google-cloud-bigtable/src/test/java/com/google/cloud/bigtable/data/v2/it/ReadModifyWriteIT.java index e00556211f..ef5cf83c75 100644 --- a/google-cloud-bigtable/src/test/java/com/google/cloud/bigtable/data/v2/it/ReadModifyWriteIT.java +++ b/google-cloud-bigtable/src/test/java/com/google/cloud/bigtable/data/v2/it/ReadModifyWriteIT.java @@ -15,10 +15,19 @@ */ package com.google.cloud.bigtable.data.v2.it; +import static com.google.cloud.bigtable.misc_utilities.AuthorizedViewTestHelper.AUTHORIZED_VIEW_COLUMN_QUALIFIER; +import static com.google.cloud.bigtable.misc_utilities.AuthorizedViewTestHelper.AUTHORIZED_VIEW_ROW_PREFIX; +import static com.google.cloud.bigtable.misc_utilities.AuthorizedViewTestHelper.createTestAuthorizedView; import static com.google.common.truth.Truth.assertThat; +import static com.google.common.truth.TruthJUnit.assume; +import static org.junit.Assert.fail; +import com.google.api.gax.rpc.PermissionDeniedException; +import com.google.cloud.bigtable.admin.v2.models.AuthorizedView; +import com.google.cloud.bigtable.data.v2.models.AuthorizedViewId; import com.google.cloud.bigtable.data.v2.models.ReadModifyWriteRow; import com.google.cloud.bigtable.data.v2.models.Row; +import com.google.cloud.bigtable.test_helpers.env.EmulatorEnv; import com.google.cloud.bigtable.test_helpers.env.TestEnvRule; import com.google.protobuf.ByteString; import java.util.UUID; @@ -58,4 +67,75 @@ public void test() throws InterruptedException, ExecutionException, TimeoutExcep assertThat(row.getCells().get(2).getValue()) .isEqualTo(ByteString.copyFrom(new byte[] {0, 0, 0, 0, 0x12, 0x34, 0x56, 0x79})); } + + @Test + public void testOnAuthorizedView() + throws InterruptedException, ExecutionException, TimeoutException { + assume() + .withMessage("AuthorizedView is not supported on Emulator") + .that(testEnvRule.env()) + .isNotInstanceOf(EmulatorEnv.class); + + AuthorizedView testAuthorizedView = createTestAuthorizedView(testEnvRule); + + String tableId = testEnvRule.env().getTableId(); + String family = testEnvRule.env().getFamilyId(); + String rowKey = AUTHORIZED_VIEW_ROW_PREFIX + UUID.randomUUID(); + + Row row = + testEnvRule + .env() + .getDataClient() + .readModifyWriteRowAsync( + ReadModifyWriteRow.create( + AuthorizedViewId.of(tableId, testAuthorizedView.getId()), rowKey) + .append(family, AUTHORIZED_VIEW_COLUMN_QUALIFIER + "1", "a") + .increment(family, AUTHORIZED_VIEW_COLUMN_QUALIFIER + "2", 3) + .increment(family, AUTHORIZED_VIEW_COLUMN_QUALIFIER + "3", 0x12345679)) + .get(1, TimeUnit.MINUTES); + + assertThat(row.getCells()).hasSize(3); + assertThat(row.getCells().get(0).getValue()).isEqualTo(ByteString.copyFromUtf8("a")); + assertThat(row.getCells().get(1).getValue()) + .isEqualTo(ByteString.copyFrom(new byte[] {0, 0, 0, 0, 0, 0, 0, 3})); + assertThat(row.getCells().get(2).getValue()) + .isEqualTo(ByteString.copyFrom(new byte[] {0, 0, 0, 0, 0x12, 0x34, 0x56, 0x79})); + + // Row key outside the authorized view + String rowKeyOutsideAuthorizedView = UUID.randomUUID() + "-outside-authorized-view"; + try { + testEnvRule + .env() + .getDataClient() + .readModifyWriteRowAsync( + ReadModifyWriteRow.create( + AuthorizedViewId.of(tableId, testAuthorizedView.getId()), + rowKeyOutsideAuthorizedView) + .append(family, AUTHORIZED_VIEW_COLUMN_QUALIFIER, "a")) + .get(1, TimeUnit.MINUTES); + fail("Should not be able to modify a row outside authorized view"); + } catch (Exception e) { + assertThat(e.getCause()).isInstanceOf(PermissionDeniedException.class); + } + + // Column qualifier outside the authorized view + try { + testEnvRule + .env() + .getDataClient() + .readModifyWriteRowAsync( + ReadModifyWriteRow.create( + AuthorizedViewId.of(tableId, testAuthorizedView.getId()), rowKey) + .append(family, "outside-authorized-view", "a")) + .get(1, TimeUnit.MINUTES); + fail("Should not be able to perform mutations with cells outside the authorized view"); + } catch (Exception e) { + assertThat(e.getCause()).isInstanceOf(PermissionDeniedException.class); + } + + testEnvRule + .env() + .getTableAdminClient() + .deleteAuthorizedView(testEnvRule.env().getTableId(), testAuthorizedView.getId()); + } } diff --git a/google-cloud-bigtable/src/test/java/com/google/cloud/bigtable/data/v2/it/RowMutationEntryBatcherIT.java b/google-cloud-bigtable/src/test/java/com/google/cloud/bigtable/data/v2/it/RowMutationEntryBatcherIT.java index 4191a01ea6..6b2eaf2047 100644 --- a/google-cloud-bigtable/src/test/java/com/google/cloud/bigtable/data/v2/it/RowMutationEntryBatcherIT.java +++ b/google-cloud-bigtable/src/test/java/com/google/cloud/bigtable/data/v2/it/RowMutationEntryBatcherIT.java @@ -15,15 +15,23 @@ */ package com.google.cloud.bigtable.data.v2.it; +import static com.google.cloud.bigtable.misc_utilities.AuthorizedViewTestHelper.AUTHORIZED_VIEW_COLUMN_QUALIFIER; +import static com.google.cloud.bigtable.misc_utilities.AuthorizedViewTestHelper.AUTHORIZED_VIEW_ROW_PREFIX; +import static com.google.cloud.bigtable.misc_utilities.AuthorizedViewTestHelper.createTestAuthorizedView; import static com.google.common.truth.Truth.assertThat; +import static com.google.common.truth.TruthJUnit.assume; +import static org.junit.Assert.fail; import com.google.api.gax.batching.Batcher; import com.google.api.gax.rpc.ServerStream; +import com.google.cloud.bigtable.admin.v2.models.AuthorizedView; import com.google.cloud.bigtable.data.v2.BigtableDataClient; +import com.google.cloud.bigtable.data.v2.models.AuthorizedViewId; import com.google.cloud.bigtable.data.v2.models.Query; import com.google.cloud.bigtable.data.v2.models.Row; import com.google.cloud.bigtable.data.v2.models.RowCell; import com.google.cloud.bigtable.data.v2.models.RowMutationEntry; +import com.google.cloud.bigtable.test_helpers.env.EmulatorEnv; import com.google.cloud.bigtable.test_helpers.env.TestEnvRule; import com.google.common.collect.ImmutableList; import com.google.protobuf.ByteString; @@ -72,4 +80,64 @@ public void testNewBatcher() throws Exception { assertThat(actualRows).containsExactlyElementsIn(expectedRows); } + + @Test + public void testNewBatcherOnAuthorizedView() throws Exception { + assume() + .withMessage("AuthorizedView is not supported on Emulator") + .that(testEnvRule.env()) + .isNotInstanceOf(EmulatorEnv.class); + + AuthorizedView testAuthorizedView = createTestAuthorizedView(testEnvRule); + + BigtableDataClient client = testEnvRule.env().getDataClient(); + String tableId = testEnvRule.env().getTableId(); + String family = testEnvRule.env().getFamilyId(); + String rowPrefix = AUTHORIZED_VIEW_ROW_PREFIX + UUID.randomUUID(); + + try (Batcher batcher = + client.newBulkMutationBatcher(AuthorizedViewId.of(tableId, testAuthorizedView.getId()))) { + for (int i = 0; i < 10; i++) { + batcher.add( + RowMutationEntry.create(rowPrefix + "-" + i) + .setCell(family, AUTHORIZED_VIEW_COLUMN_QUALIFIER, 10_000L, "value-" + i)); + } + } + + List expectedRows = new ArrayList<>(); + for (int i = 0; i < 10; i++) { + expectedRows.add( + Row.create( + ByteString.copyFromUtf8(rowPrefix + "-" + i), + ImmutableList.of( + RowCell.create( + family, + ByteString.copyFromUtf8(AUTHORIZED_VIEW_COLUMN_QUALIFIER), + 10_000L, + ImmutableList.of(), + ByteString.copyFromUtf8("value-" + i))))); + } + ServerStream actualRows = client.readRows(Query.create(tableId).prefix(rowPrefix)); + + assertThat(actualRows).containsExactlyElementsIn(expectedRows); + + // We should not be able to mutate rows outside the authorized view + String rowKeyOutsideAuthorizedView = UUID.randomUUID() + "-outside-authorized-view"; + try { + try (Batcher batcher = + client.newBulkMutationBatcher(AuthorizedViewId.of(tableId, testAuthorizedView.getId()))) { + batcher.add( + RowMutationEntry.create(rowKeyOutsideAuthorizedView) + .setCell(family, AUTHORIZED_VIEW_COLUMN_QUALIFIER, 10_000L, "value")); + } + fail("Should not be able to apply bulk mutation on rows outside authorized view"); + } catch (Exception e) { + // Ignore. + } + + testEnvRule + .env() + .getTableAdminClient() + .deleteAuthorizedView(testEnvRule.env().getTableId(), testAuthorizedView.getId()); + } } diff --git a/google-cloud-bigtable/src/test/java/com/google/cloud/bigtable/data/v2/it/SampleRowsIT.java b/google-cloud-bigtable/src/test/java/com/google/cloud/bigtable/data/v2/it/SampleRowsIT.java index cb06243509..81fd553c8e 100644 --- a/google-cloud-bigtable/src/test/java/com/google/cloud/bigtable/data/v2/it/SampleRowsIT.java +++ b/google-cloud-bigtable/src/test/java/com/google/cloud/bigtable/data/v2/it/SampleRowsIT.java @@ -15,13 +15,19 @@ */ package com.google.cloud.bigtable.data.v2.it; +import static com.google.cloud.bigtable.misc_utilities.AuthorizedViewTestHelper.AUTHORIZED_VIEW_COLUMN_QUALIFIER; +import static com.google.cloud.bigtable.misc_utilities.AuthorizedViewTestHelper.AUTHORIZED_VIEW_ROW_PREFIX; +import static com.google.cloud.bigtable.misc_utilities.AuthorizedViewTestHelper.createTestAuthorizedView; import static com.google.common.truth.Truth.assertThat; +import static com.google.common.truth.TruthJUnit.assume; import com.google.api.core.ApiFuture; import com.google.api.core.ApiFutures; +import com.google.cloud.bigtable.admin.v2.models.AuthorizedView; import com.google.cloud.bigtable.data.v2.BigtableDataClient; import com.google.cloud.bigtable.data.v2.models.KeyOffset; import com.google.cloud.bigtable.data.v2.models.RowMutation; +import com.google.cloud.bigtable.test_helpers.env.EmulatorEnv; import com.google.cloud.bigtable.test_helpers.env.TestEnvRule; import com.google.common.collect.Lists; import java.util.List; @@ -61,4 +67,50 @@ public void test() throws InterruptedException, ExecutionException, TimeoutExcep assertThat(results).isNotEmpty(); assertThat(results.get(results.size() - 1).getOffsetBytes()).isGreaterThan(0L); } + + @Test + public void testOnAuthorizedView() + throws InterruptedException, ExecutionException, TimeoutException { + assume() + .withMessage("AuthorizedView is not supported on Emulator") + .that(testEnvRule.env()) + .isNotInstanceOf(EmulatorEnv.class); + + AuthorizedView testAuthorizedView = createTestAuthorizedView(testEnvRule); + + BigtableDataClient client = testEnvRule.env().getDataClient(); + String rowPrefix = AUTHORIZED_VIEW_ROW_PREFIX + UUID.randomUUID(); + String rowPrefixOutsideAuthorizedView = UUID.randomUUID() + "-outside-authorized-view"; + + // Create some data so that sample row keys has something to show + List> futures = Lists.newArrayList(); + for (int i = 0; i < 10; i++) { + ApiFuture future = + client.mutateRowAsync( + RowMutation.create(testEnvRule.env().getTableId(), rowPrefix + "-" + i) + .setCell( + testEnvRule.env().getFamilyId(), AUTHORIZED_VIEW_COLUMN_QUALIFIER, "value")); + futures.add(future); + ApiFuture futureOutsideAuthorizedView = + client.mutateRowAsync( + RowMutation.create( + testEnvRule.env().getTableId(), rowPrefixOutsideAuthorizedView + "-" + i) + .setCell( + testEnvRule.env().getFamilyId(), AUTHORIZED_VIEW_COLUMN_QUALIFIER, "value")); + futures.add(futureOutsideAuthorizedView); + } + ApiFutures.allAsList(futures).get(1, TimeUnit.MINUTES); + + ApiFuture> future = client.sampleRowKeysAsync(testEnvRule.env().getTableId()); + + List results = future.get(1, TimeUnit.MINUTES); + + assertThat(results).isNotEmpty(); + assertThat(results.get(results.size() - 1).getOffsetBytes()).isGreaterThan(0L); + + testEnvRule + .env() + .getTableAdminClient() + .deleteAuthorizedView(testEnvRule.env().getTableId(), testAuthorizedView.getId()); + } } diff --git a/google-cloud-bigtable/src/test/java/com/google/cloud/bigtable/data/v2/models/AuthorizedViewIdTest.java b/google-cloud-bigtable/src/test/java/com/google/cloud/bigtable/data/v2/models/AuthorizedViewIdTest.java new file mode 100644 index 0000000000..b20a99ec11 --- /dev/null +++ b/google-cloud-bigtable/src/test/java/com/google/cloud/bigtable/data/v2/models/AuthorizedViewIdTest.java @@ -0,0 +1,69 @@ +/* + * Copyright 2024 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.models; + +import static com.google.common.truth.Truth.assertThat; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + +@RunWith(JUnit4.class) +public class AuthorizedViewIdTest { + private static final String PROJECT_ID = "my-project"; + private static final String INSTANCE_ID = "my-instance"; + private static final String TABLE_ID = "my-table"; + private static final String AUTHORIZED_VIEW_ID = "my-authorized-view"; + + @Test + public void testToResourceName() { + AuthorizedViewId authorizedViewId = AuthorizedViewId.of(TABLE_ID, AUTHORIZED_VIEW_ID); + + assertThat(authorizedViewId.toResourceName(PROJECT_ID, INSTANCE_ID)) + .isEqualTo( + "projects/my-project/instances/my-instance/tables/my-table/authorizedViews/my-authorized-view"); + } + + @Test + public void testEquality() { + AuthorizedViewId authorizedViewId = AuthorizedViewId.of(TABLE_ID, AUTHORIZED_VIEW_ID); + + assertThat(authorizedViewId).isEqualTo(AuthorizedViewId.of(TABLE_ID, AUTHORIZED_VIEW_ID)); + assertThat(authorizedViewId) + .isNotEqualTo(AuthorizedViewId.of(TABLE_ID, "another-authorized-view")); + assertThat(authorizedViewId).isNotEqualTo(TableId.of(TABLE_ID)); + } + + @Test + public void testHashCode() { + AuthorizedViewId authorizedViewId = AuthorizedViewId.of(TABLE_ID, AUTHORIZED_VIEW_ID); + + assertThat(authorizedViewId.hashCode()) + .isEqualTo(AuthorizedViewId.of(TABLE_ID, AUTHORIZED_VIEW_ID).hashCode()); + assertThat(authorizedViewId.hashCode()) + .isNotEqualTo(AuthorizedViewId.of(TABLE_ID, "another-authorized-view").hashCode()); + assertThat(authorizedViewId.hashCode()).isNotEqualTo(TableId.of(TABLE_ID).hashCode()); + } + + @Test + public void testToString() { + AuthorizedViewId authorizedViewId = AuthorizedViewId.of(TABLE_ID, AUTHORIZED_VIEW_ID); + + assertThat(authorizedViewId.toString()) + .isEqualTo("AuthorizedViewId{tableId=my-table, authorizedViewId=my-authorized-view}"); + } +} diff --git a/google-cloud-bigtable/src/test/java/com/google/cloud/bigtable/data/v2/models/BulkMutationTest.java b/google-cloud-bigtable/src/test/java/com/google/cloud/bigtable/data/v2/models/BulkMutationTest.java index 0e4c992648..84108d4a78 100644 --- a/google-cloud-bigtable/src/test/java/com/google/cloud/bigtable/data/v2/models/BulkMutationTest.java +++ b/google-cloud-bigtable/src/test/java/com/google/cloud/bigtable/data/v2/models/BulkMutationTest.java @@ -38,12 +38,14 @@ public class BulkMutationTest { private static final String PROJECT_ID = "fake-project"; private static final String INSTANCE_ID = "fake-instance"; private static final String TABLE_ID = "fake-table"; + private static final String AUTHORIZED_VIEW_ID = "fake-authorized-view"; private static final String APP_PROFILE = "fake-profile"; private static final RequestContext REQUEST_CONTEXT = RequestContext.create(PROJECT_ID, INSTANCE_ID, APP_PROFILE); @Test public void test() throws ParseException { + // Test BulkMutation on a table. BulkMutation m = BulkMutation.create(TABLE_ID) .add( @@ -95,10 +97,34 @@ public void test() throws ParseException { expected); assertThat(actual).isEqualTo(expected.build()); + + // Test BulkMutation on an authorized view. + m = + BulkMutation.create(AuthorizedViewId.of(TABLE_ID, AUTHORIZED_VIEW_ID)) + .add( + "key-a", + Mutation.create() + .setCell("fake-family1", "fake-qualifier1", 1_000, "fake-value1") + .setCell("fake-family2", "fake-qualifier2", 2_000, "fake-value2")) + .add( + ByteString.copyFromUtf8("key-b"), + Mutation.create().setCell("fake-family3", "fake-qualifier3", 3_000, "fake-value3")); + + actual = m.toProto(REQUEST_CONTEXT); + + expected + .clearTableName() + .setAuthorizedViewName( + NameUtil.formatAuthorizedViewName( + PROJECT_ID, INSTANCE_ID, TABLE_ID, AUTHORIZED_VIEW_ID)) + .setAppProfileId(APP_PROFILE); + + assertThat(actual).isEqualTo(expected.build()); } @Test public void serializationTest() throws IOException, ClassNotFoundException { + // Test BulkMutation on a table. BulkMutation expected = BulkMutation.create(TABLE_ID) .add( @@ -114,29 +140,82 @@ public void serializationTest() throws IOException, ClassNotFoundException { BulkMutation actual = (BulkMutation) ois.readObject(); assertThat(actual.toProto(REQUEST_CONTEXT)).isEqualTo(expected.toProto(REQUEST_CONTEXT)); + + // Test BulkMutation on an authorized view. + expected = + BulkMutation.create(AuthorizedViewId.of(TABLE_ID, AUTHORIZED_VIEW_ID)) + .add( + "key-a", + Mutation.create().setCell("fake-family1", "fake-qualifier1", 1_000, "fake-value1")); + + bos = new ByteArrayOutputStream(); + oos = new ObjectOutputStream(bos); + oos.writeObject(expected); + oos.close(); + + ois = new ObjectInputStream(new ByteArrayInputStream(bos.toByteArray())); + + actual = (BulkMutation) ois.readObject(); + assertThat(actual.toProto(REQUEST_CONTEXT)).isEqualTo(expected.toProto(REQUEST_CONTEXT)); } @Test public void cloneTest() { - BulkMutation originalBulkMutation = + // Test BulkMutation on a table. + BulkMutation originalTableBulkMutation = BulkMutation.create(TABLE_ID) .add( "test-rowKey", Mutation.create().setCell("fake-family1", "fake-qualifier1", 12345, "fake-value1")); - MutateRowsRequest originalRequest = originalBulkMutation.toProto(REQUEST_CONTEXT); - BulkMutation clonedMutation = originalBulkMutation.clone(); - MutateRowsRequest clonedRequest = clonedMutation.toProto(REQUEST_CONTEXT); + MutateRowsRequest originalTableRequest = originalTableBulkMutation.toProto(REQUEST_CONTEXT); + BulkMutation clonedTableMutation = originalTableBulkMutation.clone(); + MutateRowsRequest clonedTableRequest = clonedTableMutation.toProto(REQUEST_CONTEXT); + + // Both BulkMutations should be equals. + assertThat(clonedTableRequest).isEqualTo(originalTableRequest); + assertThat(clonedTableRequest.getTableName()).isEqualTo(originalTableRequest.getTableName()); + assertThat(clonedTableRequest.getAuthorizedViewName()) + .isEqualTo(originalTableRequest.getAuthorizedViewName()); + assertThat(clonedTableRequest.getEntriesList()) + .isEqualTo(originalTableRequest.getEntriesList()); + + // Mutating cloned BulkMutation + clonedTableMutation.add( + "another-rowKey", Mutation.create().deleteCells("delete-family", "delete-qualifier")); + assertThat(clonedTableMutation.toProto(REQUEST_CONTEXT)).isNotEqualTo(originalTableRequest); + + // Test BulkMutation on an authorized view. + BulkMutation originalAuthorizedViewBulkMutation = + BulkMutation.create(AuthorizedViewId.of(TABLE_ID, AUTHORIZED_VIEW_ID)) + .add( + "test-rowKey", + Mutation.create().setCell("fake-family1", "fake-qualifier1", 12345, "fake-value1")); + + MutateRowsRequest originalAuthorizedViewRequest = + originalAuthorizedViewBulkMutation.toProto(REQUEST_CONTEXT); + BulkMutation clonedAuthorizedViewMutation = originalAuthorizedViewBulkMutation.clone(); + MutateRowsRequest clonedAuthorizedViewRequest = + clonedAuthorizedViewMutation.toProto(REQUEST_CONTEXT); // Both BulkMutations should be equals. - assertThat(clonedRequest).isEqualTo(originalRequest); - assertThat(clonedRequest.getTableName()).isEqualTo(originalRequest.getTableName()); - assertThat(clonedRequest.getEntriesList()).isEqualTo(originalRequest.getEntriesList()); + assertThat(clonedAuthorizedViewRequest).isEqualTo(originalAuthorizedViewRequest); + assertThat(clonedAuthorizedViewRequest.getTableName()) + .isEqualTo(originalAuthorizedViewRequest.getTableName()); + assertThat(clonedAuthorizedViewRequest.getAuthorizedViewName()) + .isEqualTo(originalAuthorizedViewRequest.getAuthorizedViewName()); + assertThat(clonedAuthorizedViewRequest.getEntriesList()) + .isEqualTo(originalAuthorizedViewRequest.getEntriesList()); // Mutating cloned BulkMutation - clonedMutation.add( + clonedAuthorizedViewMutation.add( "another-rowKey", Mutation.create().deleteCells("delete-family", "delete-qualifier")); - assertThat(clonedMutation.toProto(REQUEST_CONTEXT)).isNotEqualTo(originalRequest); + assertThat(clonedAuthorizedViewMutation.toProto(REQUEST_CONTEXT)) + .isNotEqualTo(originalAuthorizedViewRequest); + + // BulkMutations on an authorized view is different from BulkMutations on a table. + assertThat(originalAuthorizedViewRequest).isNotEqualTo(originalTableRequest); + assertThat(clonedAuthorizedViewRequest).isNotEqualTo(clonedTableRequest); } @Test @@ -144,13 +223,21 @@ public void addRowMutationEntry() { RowMutationEntry entry = RowMutationEntry.create("test-rowKey") .setCell("fake-family1", "fake-qualifier1", "fake-value1"); + + // Test BulkMutation on a table. BulkMutation bulkMutation = BulkMutation.create(TABLE_ID); bulkMutation.add(entry); assertThat(bulkMutation.toProto(REQUEST_CONTEXT).getEntriesList()).contains(entry.toProto()); + + // Test BulkMutation on an authorized view. + bulkMutation = BulkMutation.create(AuthorizedViewId.of(TABLE_ID, AUTHORIZED_VIEW_ID)); + bulkMutation.add(entry); + assertThat(bulkMutation.toProto(REQUEST_CONTEXT).getEntriesList()).contains(entry.toProto()); } @Test public void fromProtoTest() { + // Test BulkMutation on a table. BulkMutation expected = BulkMutation.create(TABLE_ID) .add( @@ -171,6 +258,29 @@ public void fromProtoTest() { assertThat(overriddenRequest).isNotEqualTo(protoRequest); assertThat(overriddenRequest.getTableName()) .matches(NameUtil.formatTableName(projectId, instanceId, TABLE_ID)); + assertThat(overriddenRequest.getAuthorizedViewName()).isEmpty(); + assertThat(overriddenRequest.getAppProfileId()).matches(appProfile); + + // Test BulkMutation on an authorized view. + expected = + BulkMutation.create(AuthorizedViewId.of(TABLE_ID, AUTHORIZED_VIEW_ID)) + .add( + "key", + Mutation.create().setCell("fake-family", "fake-qualifier", 10_000L, "fake-value")); + + protoRequest = expected.toProto(REQUEST_CONTEXT); + actualBulkMutation = BulkMutation.fromProto(protoRequest); + + assertThat(actualBulkMutation.toProto(REQUEST_CONTEXT)).isEqualTo(protoRequest); + + overriddenRequest = + actualBulkMutation.toProto(RequestContext.create(projectId, instanceId, appProfile)); + + assertThat(overriddenRequest).isNotEqualTo(protoRequest); + assertThat(overriddenRequest.getTableName()).isEmpty(); + assertThat(overriddenRequest.getAuthorizedViewName()) + .matches( + NameUtil.formatAuthorizedViewName(projectId, instanceId, TABLE_ID, AUTHORIZED_VIEW_ID)); assertThat(overriddenRequest.getAppProfileId()).matches(appProfile); } diff --git a/google-cloud-bigtable/src/test/java/com/google/cloud/bigtable/data/v2/models/ConditionalRowMutationTest.java b/google-cloud-bigtable/src/test/java/com/google/cloud/bigtable/data/v2/models/ConditionalRowMutationTest.java index 8a626bb846..0f4e11a162 100644 --- a/google-cloud-bigtable/src/test/java/com/google/cloud/bigtable/data/v2/models/ConditionalRowMutationTest.java +++ b/google-cloud-bigtable/src/test/java/com/google/cloud/bigtable/data/v2/models/ConditionalRowMutationTest.java @@ -28,6 +28,8 @@ import java.io.IOException; import java.io.ObjectInputStream; import java.io.ObjectOutputStream; +import java.util.ArrayList; +import java.util.List; import org.junit.Test; import org.junit.runner.RunWith; import org.junit.runners.JUnit4; @@ -37,6 +39,7 @@ public class ConditionalRowMutationTest { private static final String PROJECT_ID = "fake-project"; private static final String INSTANCE_ID = "fake-instance"; private static final String TABLE_ID = "fake-table"; + private static final String AUTHORIZED_VIEW_ID = "fake-authorized-view"; private static final String APP_PROFILE_ID = "fake-profile"; private static final RequestContext REQUEST_CONTEXT = @@ -47,6 +50,8 @@ public class ConditionalRowMutationTest { @Test public void toProtoTest() { Mutation ignoredThenMutation = Mutation.create().deleteRow(); + + // Test ConditionalRowMutation on a table. ConditionalRowMutation mutation = ConditionalRowMutation.create(TABLE_ID, TEST_KEY).then(ignoredThenMutation); @@ -60,10 +65,28 @@ public void toProtoTest() { .setAppProfileId(APP_PROFILE_ID) .setRowKey(TEST_KEY) .build()); + + // Test ConditionalRowMutation on an authorized view. + mutation = + ConditionalRowMutation.create(AuthorizedViewId.of(TABLE_ID, AUTHORIZED_VIEW_ID), TEST_KEY) + .then(ignoredThenMutation); + + actualProto = mutation.toProto(REQUEST_CONTEXT).toBuilder().clearTrueMutations().build(); + + assertThat(actualProto) + .isEqualTo( + CheckAndMutateRowRequest.newBuilder() + .setAuthorizedViewName( + NameUtil.formatAuthorizedViewName( + PROJECT_ID, INSTANCE_ID, TABLE_ID, AUTHORIZED_VIEW_ID)) + .setAppProfileId(APP_PROFILE_ID) + .setRowKey(TEST_KEY) + .build()); } @Test public void conditionTest() { + // Test ConditionalRowMutation on a table. ConditionalRowMutation mutation = ConditionalRowMutation.create(TABLE_ID, TEST_KEY) .condition(Filters.FILTERS.key().regex("a.*")) @@ -71,6 +94,18 @@ public void conditionTest() { CheckAndMutateRowRequest actualProto = mutation.toProto(REQUEST_CONTEXT); + assertThat(actualProto.getPredicateFilter()) + .isEqualTo( + RowFilter.newBuilder().setRowKeyRegexFilter(ByteString.copyFromUtf8("a.*")).build()); + + // Test ConditionalRowMutation on an authorized view. + mutation = + ConditionalRowMutation.create(AuthorizedViewId.of(TABLE_ID, AUTHORIZED_VIEW_ID), TEST_KEY) + .condition(Filters.FILTERS.key().regex("a.*")) + .then(Mutation.create().deleteRow()); + + actualProto = mutation.toProto(REQUEST_CONTEXT); + assertThat(actualProto.getPredicateFilter()) .isEqualTo( RowFilter.newBuilder().setRowKeyRegexFilter(ByteString.copyFromUtf8("a.*")).build()); @@ -78,6 +113,7 @@ public void conditionTest() { @Test public void thenTest() { + // Test ConditionalRowMutation on a table. ConditionalRowMutation mutation = ConditionalRowMutation.create(TABLE_ID, TEST_KEY) .then(Mutation.create().deleteCells("family1", "qualifier1")) @@ -85,25 +121,37 @@ public void thenTest() { CheckAndMutateRowRequest actualProto = mutation.toProto(REQUEST_CONTEXT); - assertThat(actualProto.getTrueMutationsList()) - .containsExactly( - com.google.bigtable.v2.Mutation.newBuilder() - .setDeleteFromColumn( - DeleteFromColumn.newBuilder() - .setFamilyName("family1") - .setColumnQualifier(ByteString.copyFromUtf8("qualifier1"))) - .build(), - com.google.bigtable.v2.Mutation.newBuilder() - .setDeleteFromColumn( - DeleteFromColumn.newBuilder() - .setFamilyName("family2") - .setColumnQualifier(ByteString.copyFromUtf8("qualifier2"))) - .build()) - .inOrder(); + List expectedMutations = new ArrayList<>(); + expectedMutations.add( + com.google.bigtable.v2.Mutation.newBuilder() + .setDeleteFromColumn( + DeleteFromColumn.newBuilder() + .setFamilyName("family1") + .setColumnQualifier(ByteString.copyFromUtf8("qualifier1"))) + .build()); + expectedMutations.add( + com.google.bigtable.v2.Mutation.newBuilder() + .setDeleteFromColumn( + DeleteFromColumn.newBuilder() + .setFamilyName("family2") + .setColumnQualifier(ByteString.copyFromUtf8("qualifier2"))) + .build()); + assertThat(actualProto.getTrueMutationsList()).isEqualTo(expectedMutations); + + // Test ConditionalRowMutation on an authorized view. + mutation = + ConditionalRowMutation.create(AuthorizedViewId.of(TABLE_ID, AUTHORIZED_VIEW_ID), TEST_KEY) + .then(Mutation.create().deleteCells("family1", "qualifier1")) + .then(Mutation.create().deleteCells("family2", "qualifier2")); + + actualProto = mutation.toProto(REQUEST_CONTEXT); + + assertThat(actualProto.getTrueMutationsList()).isEqualTo(expectedMutations); } @Test public void otherwiseTest() { + // Test ConditionalRowMutation on a table. ConditionalRowMutation mutation = ConditionalRowMutation.create(TABLE_ID, TEST_KEY) .otherwise(Mutation.create().deleteCells("family1", "qualifier1")) @@ -111,25 +159,37 @@ public void otherwiseTest() { CheckAndMutateRowRequest actualProto = mutation.toProto(REQUEST_CONTEXT); - assertThat(actualProto.getFalseMutationsList()) - .containsExactly( - com.google.bigtable.v2.Mutation.newBuilder() - .setDeleteFromColumn( - DeleteFromColumn.newBuilder() - .setFamilyName("family1") - .setColumnQualifier(ByteString.copyFromUtf8("qualifier1"))) - .build(), - com.google.bigtable.v2.Mutation.newBuilder() - .setDeleteFromColumn( - DeleteFromColumn.newBuilder() - .setFamilyName("family2") - .setColumnQualifier(ByteString.copyFromUtf8("qualifier2"))) - .build()) - .inOrder(); + List expectedMutations = new ArrayList<>(); + expectedMutations.add( + com.google.bigtable.v2.Mutation.newBuilder() + .setDeleteFromColumn( + DeleteFromColumn.newBuilder() + .setFamilyName("family1") + .setColumnQualifier(ByteString.copyFromUtf8("qualifier1"))) + .build()); + expectedMutations.add( + com.google.bigtable.v2.Mutation.newBuilder() + .setDeleteFromColumn( + DeleteFromColumn.newBuilder() + .setFamilyName("family2") + .setColumnQualifier(ByteString.copyFromUtf8("qualifier2"))) + .build()); + assertThat(actualProto.getFalseMutationsList()).isEqualTo(expectedMutations); + + // Test ConditionalRowMutation on an authorized view. + mutation = + ConditionalRowMutation.create(AuthorizedViewId.of(TABLE_ID, AUTHORIZED_VIEW_ID), TEST_KEY) + .otherwise(Mutation.create().deleteCells("family1", "qualifier1")) + .otherwise(Mutation.create().deleteCells("family2", "qualifier2")); + + actualProto = mutation.toProto(REQUEST_CONTEXT); + + assertThat(actualProto.getFalseMutationsList()).isEqualTo(expectedMutations); } @Test public void noEffectClausesTest() { + // Test ConditionalRowMutation on a table. ConditionalRowMutation mutation = ConditionalRowMutation.create(TABLE_ID, TEST_KEY).condition(Filters.FILTERS.pass()); @@ -142,10 +202,24 @@ public void noEffectClausesTest() { } assertThat(actualError).isInstanceOf(IllegalStateException.class); + + // Test ConditionalRowMutation on an authorized view. + mutation = + ConditionalRowMutation.create(AuthorizedViewId.of(TABLE_ID, AUTHORIZED_VIEW_ID), TEST_KEY) + .condition(Filters.FILTERS.pass()); + + try { + mutation.toProto(REQUEST_CONTEXT); + } catch (Throwable t) { + actualError = t; + } + + assertThat(actualError).isInstanceOf(IllegalStateException.class); } @Test public void serializationTest() throws IOException, ClassNotFoundException { + // Test ConditionalRowMutation on a table. ConditionalRowMutation expected = ConditionalRowMutation.create(TABLE_ID, TEST_KEY) .condition(Filters.FILTERS.pass()) @@ -161,10 +235,28 @@ public void serializationTest() throws IOException, ClassNotFoundException { ConditionalRowMutation actual = (ConditionalRowMutation) ois.readObject(); assertThat(actual.toProto(REQUEST_CONTEXT)).isEqualTo(expected.toProto(REQUEST_CONTEXT)); + + // Test ConditionalRowMutation on an authorized view. + expected = + ConditionalRowMutation.create(AuthorizedViewId.of(TABLE_ID, AUTHORIZED_VIEW_ID), TEST_KEY) + .condition(Filters.FILTERS.pass()) + .then(Mutation.create().deleteRow()) + .otherwise(Mutation.create().deleteFamily("cf")); + + bos = new ByteArrayOutputStream(); + oos = new ObjectOutputStream(bos); + oos.writeObject(expected); + oos.close(); + + ois = new ObjectInputStream(new ByteArrayInputStream(bos.toByteArray())); + + actual = (ConditionalRowMutation) ois.readObject(); + assertThat(actual.toProto(REQUEST_CONTEXT)).isEqualTo(expected.toProto(REQUEST_CONTEXT)); } @Test public void fromProtoTest() { + // Test ConditionalRowMutation on a table. ConditionalRowMutation mutation = ConditionalRowMutation.create(TABLE_ID, TEST_KEY) .condition(Filters.FILTERS.key().regex("test")) @@ -185,6 +277,29 @@ public void fromProtoTest() { assertThat(overriddenRequest).isNotEqualTo(protoRequest); assertThat(overriddenRequest.getTableName()) .matches(NameUtil.formatTableName(projectId, instanceId, TABLE_ID)); + assertThat(overriddenRequest.getAuthorizedViewName()).isEmpty(); + assertThat(overriddenRequest.getAppProfileId()).matches(appProfile); + + // Test ConditionalRowMutation on an authorized view. + mutation = + ConditionalRowMutation.create(AuthorizedViewId.of(TABLE_ID, AUTHORIZED_VIEW_ID), TEST_KEY) + .condition(Filters.FILTERS.key().regex("test")) + .then(Mutation.create().setCell("family1", "qualifier1", 10_000L, "value")) + .otherwise(Mutation.create().deleteFamily("family")); + + protoRequest = mutation.toProto(REQUEST_CONTEXT); + actualRequest = ConditionalRowMutation.fromProto(protoRequest); + + assertThat(actualRequest.toProto(REQUEST_CONTEXT)).isEqualTo(protoRequest); + + overriddenRequest = + actualRequest.toProto(RequestContext.create(projectId, instanceId, appProfile)); + + assertThat(overriddenRequest).isNotEqualTo(protoRequest); + assertThat(overriddenRequest.getTableName()).isEmpty(); + assertThat(overriddenRequest.getAuthorizedViewName()) + .matches( + NameUtil.formatAuthorizedViewName(projectId, instanceId, TABLE_ID, AUTHORIZED_VIEW_ID)); assertThat(overriddenRequest.getAppProfileId()).matches(appProfile); } } diff --git a/google-cloud-bigtable/src/test/java/com/google/cloud/bigtable/data/v2/models/QueryTest.java b/google-cloud-bigtable/src/test/java/com/google/cloud/bigtable/data/v2/models/QueryTest.java index 93e5b1c92f..6ba80ed767 100644 --- a/google-cloud-bigtable/src/test/java/com/google/cloud/bigtable/data/v2/models/QueryTest.java +++ b/google-cloud-bigtable/src/test/java/com/google/cloud/bigtable/data/v2/models/QueryTest.java @@ -49,6 +49,7 @@ public class QueryTest { private static final String PROJECT_ID = "fake-project"; private static final String INSTANCE_ID = "fake-instance"; private static final String TABLE_ID = "fake-table"; + private static final String AUTHORIZED_VIEW_ID = "fake-authorized-view"; private static final String APP_PROFILE_ID = "fake-profile-id"; private RequestContext requestContext; @@ -61,20 +62,26 @@ public void setUp() { @Test public void requestContextTest() { + // Table query test. Query query = Query.create(TABLE_ID); - ReadRowsRequest proto = query.toProto(requestContext); - assertThat(proto).isEqualTo(expectedProtoBuilder().build()); + assertThat(proto).isEqualTo(expectedReadFromTableProtoBuilder().build()); + + // AuthorizedView query test. + query = Query.create(AuthorizedViewId.of(TABLE_ID, AUTHORIZED_VIEW_ID)); + proto = query.toProto(requestContext); + assertThat(proto).isEqualTo(expectedReadFromAuthorizedViewProtoBuilder().build()); } @Test public void rowKeysTest() { + // Table query test. Query query = Query.create(TABLE_ID) .rowKey("simple-string") .rowKey(ByteString.copyFromUtf8("byte-string")); - ReadRowsRequest.Builder expectedProto = expectedProtoBuilder(); + ReadRowsRequest.Builder expectedProto = expectedReadFromTableProtoBuilder(); expectedProto .getRowsBuilder() .addRowKeys(ByteString.copyFromUtf8("simple-string")) @@ -82,17 +89,33 @@ public void rowKeysTest() { ReadRowsRequest actualProto = query.toProto(requestContext); assertThat(actualProto).isEqualTo(expectedProto.build()); + + // AuthorizedView query test. + query = + Query.create(AuthorizedViewId.of(TABLE_ID, AUTHORIZED_VIEW_ID)) + .rowKey("simple-string") + .rowKey(ByteString.copyFromUtf8("byte-string")); + + expectedProto = expectedReadFromAuthorizedViewProtoBuilder(); + expectedProto + .getRowsBuilder() + .addRowKeys(ByteString.copyFromUtf8("simple-string")) + .addRowKeys(ByteString.copyFromUtf8("byte-string")); + + actualProto = query.toProto(requestContext); + assertThat(actualProto).isEqualTo(expectedProto.build()); } @Test public void rowRangeTest() { + // Table query test. Query query = Query.create(TABLE_ID) .range("simple-begin", "simple-end") .range(ByteString.copyFromUtf8("byte-begin"), ByteString.copyFromUtf8("byte-end")) .range(ByteStringRange.create("range-begin", "range-end")); - Builder expectedProto = expectedProtoBuilder(); + Builder expectedProto = expectedReadFromTableProtoBuilder(); expectedProto .getRowsBuilder() .addRowRanges( @@ -110,10 +133,37 @@ public void rowRangeTest() { ReadRowsRequest actualProto = query.toProto(requestContext); assertThat(actualProto).isEqualTo(expectedProto.build()); + + // AuthorizedView query test. + query = + Query.create(AuthorizedViewId.of(TABLE_ID, AUTHORIZED_VIEW_ID)) + .range("simple-begin", "simple-end") + .range(ByteString.copyFromUtf8("byte-begin"), ByteString.copyFromUtf8("byte-end")) + .range(ByteStringRange.create("range-begin", "range-end")); + + expectedProto = expectedReadFromAuthorizedViewProtoBuilder(); + 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"))); + + actualProto = query.toProto(requestContext); + assertThat(actualProto).isEqualTo(expectedProto.build()); } @Test public void filterTestWithExceptions() { + // Table query test. Exception actualException = null; try { Query.create(TABLE_ID).filter(null); @@ -132,32 +182,73 @@ public void filterTestWithExceptions() { actualException = ex; } assertThat(actualException).hasMessageThat().contains("filter size can't be more than 20KB"); + + // AuthorizedView query test. + actualException = null; + try { + Query.create(AuthorizedViewId.of(TABLE_ID, AUTHORIZED_VIEW_ID)).filter(null); + } catch (Exception ex) { + actualException = ex; + } + assertThat(actualException).isInstanceOf(NullPointerException.class); + + actualException = null; + try { + Query.create(AuthorizedViewId.of(TABLE_ID, AUTHORIZED_VIEW_ID)) + .filter(FILTERS.value().exactMatch(largeValue)); + } catch (Exception ex) { + actualException = ex; + } + assertThat(actualException).hasMessageThat().contains("filter size can't be more than 20KB"); } @Test public void filterTest() { + // Table query test. Query query = Query.create(TABLE_ID).filter(FILTERS.key().regex(".*")); Builder expectedProto = - expectedProtoBuilder() + expectedReadFromTableProtoBuilder() .setFilter(RowFilter.newBuilder().setRowKeyRegexFilter(ByteString.copyFromUtf8(".*"))); ReadRowsRequest actualProto = query.toProto(requestContext); assertThat(actualProto).isEqualTo(expectedProto.build()); + + // AuthorizedView query test. + query = + Query.create(AuthorizedViewId.of(TABLE_ID, AUTHORIZED_VIEW_ID)) + .filter(FILTERS.key().regex(".*")); + + expectedProto = + expectedReadFromAuthorizedViewProtoBuilder() + .setFilter(RowFilter.newBuilder().setRowKeyRegexFilter(ByteString.copyFromUtf8(".*"))); + + actualProto = query.toProto(requestContext); + assertThat(actualProto).isEqualTo(expectedProto.build()); } @Test public void limitTest() { + // Table query test. Query query = Query.create(TABLE_ID).limit(10); - Builder expectedProto = expectedProtoBuilder().setRowsLimit(10); + Builder expectedProto = expectedReadFromTableProtoBuilder().setRowsLimit(10); ReadRowsRequest actualProto = query.toProto(requestContext); assertThat(actualProto).isEqualTo(expectedProto.build()); + + // AuthorizedView query test. + query = Query.create(AuthorizedViewId.of(TABLE_ID, AUTHORIZED_VIEW_ID)).limit(10); + + expectedProto = expectedReadFromAuthorizedViewProtoBuilder().setRowsLimit(10); + + actualProto = query.toProto(requestContext); + assertThat(actualProto).isEqualTo(expectedProto.build()); } @Test public void serializationTest() throws IOException, ClassNotFoundException { + // Table query test. Query expected = Query.create(TABLE_ID).filter(FILTERS.key().regex(".*")); ByteArrayOutputStream bos = new ByteArrayOutputStream(); @@ -169,10 +260,25 @@ public void serializationTest() throws IOException, ClassNotFoundException { Query actual = (Query) ois.readObject(); assertThat(actual.toProto(requestContext)).isEqualTo(expected.toProto(requestContext)); + + // AuthorizedView query test. + expected = + Query.create(AuthorizedViewId.of(TABLE_ID, AUTHORIZED_VIEW_ID)) + .filter(FILTERS.key().regex(".*")); + + bos = new ByteArrayOutputStream(); + oos = new ObjectOutputStream(bos); + oos.writeObject(expected); + oos.close(); + + ois = new ObjectInputStream(new ByteArrayInputStream(bos.toByteArray())); + actual = (Query) ois.readObject(); + assertThat(actual.toProto(requestContext)).isEqualTo(expected.toProto(requestContext)); } @Test public void shardTestSplitPoints() { + // Table query test. Query query = Query.create(TABLE_ID).range("a", "z"); SortedSet splitPoints = @@ -207,10 +313,46 @@ public void shardTestSplitPoints() { .setStartKeyClosed(ByteString.copyFromUtf8("j")) .setEndKeyOpen(ByteString.copyFromUtf8("z")))) .build()); + + // AuthorizedView query test. + query = Query.create(AuthorizedViewId.of(TABLE_ID, AUTHORIZED_VIEW_ID)).range("a", "z"); + + subQueries = query.shard(splitPoints); + + assertThat(subQueries).hasSize(2); + assertThat(subQueries.get(0).toProto(requestContext)) + .isEqualTo( + ReadRowsRequest.newBuilder() + .setAuthorizedViewName( + NameUtil.formatAuthorizedViewName( + PROJECT_ID, INSTANCE_ID, TABLE_ID, AUTHORIZED_VIEW_ID)) + .setAppProfileId(APP_PROFILE_ID) + .setRows( + RowSet.newBuilder() + .addRowRanges( + RowRange.newBuilder() + .setStartKeyClosed(ByteString.copyFromUtf8("a")) + .setEndKeyOpen(ByteString.copyFromUtf8("j")))) + .build()); + assertThat(subQueries.get(1).toProto(requestContext)) + .isEqualTo( + ReadRowsRequest.newBuilder() + .setAuthorizedViewName( + NameUtil.formatAuthorizedViewName( + PROJECT_ID, INSTANCE_ID, TABLE_ID, AUTHORIZED_VIEW_ID)) + .setAppProfileId(APP_PROFILE_ID) + .setRows( + RowSet.newBuilder() + .addRowRanges( + RowRange.newBuilder() + .setStartKeyClosed(ByteString.copyFromUtf8("j")) + .setEndKeyOpen(ByteString.copyFromUtf8("z")))) + .build()); } @Test public void shardTestKeyOffsets() { + // Table query test. Query query = Query.create(TABLE_ID).range("a", "z"); List keyOffsets = @@ -245,16 +387,60 @@ public void shardTestKeyOffsets() { .setStartKeyClosed(ByteString.copyFromUtf8("j")) .setEndKeyOpen(ByteString.copyFromUtf8("z")))) .build()); + + // AuthorizedView query test. + query = Query.create(AuthorizedViewId.of(TABLE_ID, AUTHORIZED_VIEW_ID)).range("a", "z"); + + subQueries = query.shard(keyOffsets); + + assertThat(subQueries).hasSize(2); + assertThat(subQueries.get(0).toProto(requestContext)) + .isEqualTo( + ReadRowsRequest.newBuilder() + .setAuthorizedViewName( + NameUtil.formatAuthorizedViewName( + PROJECT_ID, INSTANCE_ID, TABLE_ID, AUTHORIZED_VIEW_ID)) + .setAppProfileId(APP_PROFILE_ID) + .setRows( + RowSet.newBuilder() + .addRowRanges( + RowRange.newBuilder() + .setStartKeyClosed(ByteString.copyFromUtf8("a")) + .setEndKeyOpen(ByteString.copyFromUtf8("j")))) + .build()); + assertThat(subQueries.get(1).toProto(requestContext)) + .isEqualTo( + ReadRowsRequest.newBuilder() + .setAuthorizedViewName( + NameUtil.formatAuthorizedViewName( + PROJECT_ID, INSTANCE_ID, TABLE_ID, AUTHORIZED_VIEW_ID)) + .setAppProfileId(APP_PROFILE_ID) + .setRows( + RowSet.newBuilder() + .addRowRanges( + RowRange.newBuilder() + .setStartKeyClosed(ByteString.copyFromUtf8("j")) + .setEndKeyOpen(ByteString.copyFromUtf8("z")))) + .build()); } - private static ReadRowsRequest.Builder expectedProtoBuilder() { + private static ReadRowsRequest.Builder expectedReadFromTableProtoBuilder() { return ReadRowsRequest.newBuilder() .setTableName(NameUtil.formatTableName(PROJECT_ID, INSTANCE_ID, TABLE_ID)) .setAppProfileId(APP_PROFILE_ID); } + private static ReadRowsRequest.Builder expectedReadFromAuthorizedViewProtoBuilder() { + return ReadRowsRequest.newBuilder() + .setAuthorizedViewName( + NameUtil.formatAuthorizedViewName( + PROJECT_ID, INSTANCE_ID, TABLE_ID, AUTHORIZED_VIEW_ID)) + .setAppProfileId(APP_PROFILE_ID); + } + @Test public void testFromProto() { + // Table query test. ReadRowsRequest request = ReadRowsRequest.newBuilder() .setTableName(NameUtil.formatTableName(PROJECT_ID, INSTANCE_ID, TABLE_ID)) @@ -271,18 +457,76 @@ public void testFromProto() { Query query = Query.fromProto(request); assertThat(query.toProto(requestContext)).isEqualTo(request); + + // AuthorizedView query test. + request = + ReadRowsRequest.newBuilder() + .setAuthorizedViewName( + NameUtil.formatAuthorizedViewName( + PROJECT_ID, INSTANCE_ID, TABLE_ID, AUTHORIZED_VIEW_ID)) + .setAppProfileId(APP_PROFILE_ID) + .setFilter(RowFilter.newBuilder().setRowKeyRegexFilter(ByteString.copyFromUtf8(".*"))) + .setRows( + RowSet.newBuilder() + .addRowKeys(ByteString.copyFromUtf8("row-key")) + .addRowRanges( + RowRange.newBuilder() + .setStartKeyClosed(ByteString.copyFromUtf8("j")) + .setEndKeyClosed(ByteString.copyFromUtf8("z")))) + .build(); + query = Query.fromProto(request); + + assertThat(query.toProto(requestContext)).isEqualTo(request); } @Test(expected = IllegalArgumentException.class) - public void testFromProtoWithEmptyTableId() { - Query.fromProto(ReadRowsRequest.getDefaultInstance()); + public void testFromProtoWithInvalidTableId() { + Query.fromProto( + ReadRowsRequest.getDefaultInstance().toBuilder().setTableName("invalid-name").build()); expect.expect(IllegalArgumentException.class); expect.expectMessage("Invalid table name:"); } + @Test(expected = IllegalArgumentException.class) + public void testFromProtoWithInvalidAuthorizedViewId() { + Query.fromProto( + ReadRowsRequest.getDefaultInstance() + .toBuilder() + .setAuthorizedViewName("invalid-name") + .build()); + + expect.expect(IllegalArgumentException.class); + expect.expectMessage("Invalid authorized view name:"); + } + + @Test(expected = IllegalArgumentException.class) + public void testFromProtoWithEmptyTableAndAuthorizedViewId() { + Query.fromProto(ReadRowsRequest.getDefaultInstance()); + + expect.expect(IllegalArgumentException.class); + expect.expectMessage("Either table name or authorized view name must be specified"); + } + + @Test(expected = IllegalArgumentException.class) + public void testFromProtoWithBothTableAndAuthorizedViewId() { + Query.fromProto( + ReadRowsRequest.getDefaultInstance() + .toBuilder() + .setTableName(NameUtil.formatTableName(PROJECT_ID, INSTANCE_ID, TABLE_ID)) + .setAuthorizedViewName( + NameUtil.formatAuthorizedViewName( + PROJECT_ID, INSTANCE_ID, TABLE_ID, AUTHORIZED_VIEW_ID)) + .build()); + + expect.expect(IllegalArgumentException.class); + expect.expectMessage( + "Table name and authorized view name cannot be specified at the same time"); + } + @Test public void testEquality() { + // Table query test. Query request = Query.create(TABLE_ID) .rowKey("row-key") @@ -307,10 +551,47 @@ public void testEquality() { assertThat(Query.create(TABLE_ID).filter(FILTERS.family().regex("test"))) .isNotEqualTo(Query.create(TABLE_ID).filter(FILTERS.family().exactMatch("test-one"))); assertThat(Query.create(TABLE_ID).limit(4)).isNotEqualTo(Query.create(TABLE_ID).limit(5)); + + // AuthorizedView query test. + request = + Query.create(AuthorizedViewId.of(TABLE_ID, AUTHORIZED_VIEW_ID)) + .rowKey("row-key") + .range("a", "z") + .limit(3) + .filter(FILTERS.family().exactMatch("test")); + + // Query#toProto should not change the Query instance state + request.toProto(requestContext); + assertThat(request) + .isEqualTo( + Query.create(AuthorizedViewId.of(TABLE_ID, AUTHORIZED_VIEW_ID)) + .rowKey("row-key") + .range("a", "z") + .limit(3) + .filter(FILTERS.family().exactMatch("test"))); + + assertThat(Query.create(AuthorizedViewId.of(TABLE_ID, AUTHORIZED_VIEW_ID)).rowKey("row-key")) + .isNotEqualTo( + Query.create(AuthorizedViewId.of(TABLE_ID, AUTHORIZED_VIEW_ID)).rowKey("row-key-1")); + assertThat(Query.create(AuthorizedViewId.of(TABLE_ID, AUTHORIZED_VIEW_ID)).range("a", "z")) + .isNotEqualTo( + Query.create(AuthorizedViewId.of(TABLE_ID, AUTHORIZED_VIEW_ID)).range("a", "s")); + assertThat( + Query.create(AuthorizedViewId.of(TABLE_ID, AUTHORIZED_VIEW_ID)) + .filter(FILTERS.family().regex("test"))) + .isNotEqualTo( + Query.create(AuthorizedViewId.of(TABLE_ID, AUTHORIZED_VIEW_ID)) + .filter(FILTERS.family().exactMatch("test-one"))); + assertThat(Query.create(AuthorizedViewId.of(TABLE_ID, AUTHORIZED_VIEW_ID)).limit(4)) + .isNotEqualTo(Query.create(AuthorizedViewId.of(TABLE_ID, AUTHORIZED_VIEW_ID)).limit(5)); + + assertThat(Query.create(AuthorizedViewId.of(TABLE_ID, AUTHORIZED_VIEW_ID))) + .isNotEqualTo(Query.create(TABLE_ID)); } @Test public void testClone() { + // Table query test. Query query = Query.create(TABLE_ID).filter(FILTERS.key().regex("temp")).limit(10); ReadRowsRequest request = ReadRowsRequest.newBuilder() @@ -326,10 +607,33 @@ public void testClone() { Query clonedReq = query.clone(); assertThat(clonedReq).isEqualTo(query); assertThat(clonedReq.toProto(requestContext)).isEqualTo(request); + + // AuthorizedView query test. + query = + Query.create(AuthorizedViewId.of(TABLE_ID, AUTHORIZED_VIEW_ID)) + .filter(FILTERS.key().regex("temp")) + .limit(10); + request = + ReadRowsRequest.newBuilder() + .setAuthorizedViewName( + NameUtil.formatAuthorizedViewName( + PROJECT_ID, INSTANCE_ID, TABLE_ID, AUTHORIZED_VIEW_ID)) + .setAppProfileId(APP_PROFILE_ID) + .setRowsLimit(10) + .setFilter( + RowFilter.newBuilder() + .setRowKeyRegexFilter(ByteString.copyFromUtf8("temp")) + .build()) + .build(); + + clonedReq = query.clone(); + assertThat(clonedReq).isEqualTo(query); + assertThat(clonedReq.toProto(requestContext)).isEqualTo(request); } @Test public void testQueryPaginatorRangeLimitReached() { + // Table query test. int chunkSize = 10, limit = 15; Query query = Query.create(TABLE_ID).range("a", "z").limit(limit); Query.QueryPaginator paginator = query.createPaginator(chunkSize); @@ -337,7 +641,7 @@ public void testQueryPaginatorRangeLimitReached() { Query nextQuery = paginator.getNextQuery(); Builder expectedProto = - expectedProtoBuilder() + expectedReadFromTableProtoBuilder() .setRows( RowSet.newBuilder() .addRowRanges( @@ -352,7 +656,44 @@ public void testQueryPaginatorRangeLimitReached() { int expectedLimit = limit - chunkSize; nextQuery = paginator.getNextQuery(); expectedProto = - expectedProtoBuilder() + expectedReadFromTableProtoBuilder() + .setRows( + RowSet.newBuilder() + .addRowRanges( + RowRange.newBuilder() + .setStartKeyOpen(ByteString.copyFromUtf8("c")) + .setEndKeyOpen(ByteString.copyFromUtf8("z")) + .build())) + .setRowsLimit(expectedLimit); + assertThat(nextQuery.toProto(requestContext)).isEqualTo(expectedProto.build()); + + assertThat(paginator.advance(ByteString.copyFromUtf8("d"))).isFalse(); + + // AuthorizedView query test. + query = + Query.create(AuthorizedViewId.of(TABLE_ID, AUTHORIZED_VIEW_ID)) + .range("a", "z") + .limit(limit); + paginator = query.createPaginator(chunkSize); + + nextQuery = paginator.getNextQuery(); + + expectedProto = + expectedReadFromAuthorizedViewProtoBuilder() + .setRows( + RowSet.newBuilder() + .addRowRanges( + RowRange.newBuilder() + .setStartKeyClosed(ByteString.copyFromUtf8("a")) + .setEndKeyOpen(ByteString.copyFromUtf8("z")) + .build())) + .setRowsLimit(chunkSize); + assertThat(nextQuery.toProto(requestContext)).isEqualTo(expectedProto.build()); + + assertThat(paginator.advance(ByteString.copyFromUtf8("c"))).isTrue(); + nextQuery = paginator.getNextQuery(); + expectedProto = + expectedReadFromAuthorizedViewProtoBuilder() .setRows( RowSet.newBuilder() .addRowRanges( @@ -369,13 +710,15 @@ public void testQueryPaginatorRangeLimitReached() { @Test public void testQueryPaginatorRangeLimitMultiplyOfChunkSize() { int chunkSize = 10, limit = 20; + + // Table query test. Query query = Query.create(TABLE_ID).range("a", "z").limit(limit); Query.QueryPaginator paginator = query.createPaginator(chunkSize); Query nextQuery = paginator.getNextQuery(); Builder expectedProto = - expectedProtoBuilder() + expectedReadFromTableProtoBuilder() .setRows( RowSet.newBuilder() .addRowRanges( @@ -390,7 +733,44 @@ public void testQueryPaginatorRangeLimitMultiplyOfChunkSize() { int expectedLimit = limit - chunkSize; nextQuery = paginator.getNextQuery(); expectedProto = - expectedProtoBuilder() + expectedReadFromTableProtoBuilder() + .setRows( + RowSet.newBuilder() + .addRowRanges( + RowRange.newBuilder() + .setStartKeyOpen(ByteString.copyFromUtf8("c")) + .setEndKeyOpen(ByteString.copyFromUtf8("z")) + .build())) + .setRowsLimit(expectedLimit); + assertThat(nextQuery.toProto(requestContext)).isEqualTo(expectedProto.build()); + + assertThat(paginator.advance(ByteString.copyFromUtf8("d"))).isFalse(); + + // AuthorizedView query test. + query = + Query.create(AuthorizedViewId.of(TABLE_ID, AUTHORIZED_VIEW_ID)) + .range("a", "z") + .limit(limit); + paginator = query.createPaginator(chunkSize); + + nextQuery = paginator.getNextQuery(); + + expectedProto = + expectedReadFromAuthorizedViewProtoBuilder() + .setRows( + RowSet.newBuilder() + .addRowRanges( + RowRange.newBuilder() + .setStartKeyClosed(ByteString.copyFromUtf8("a")) + .setEndKeyOpen(ByteString.copyFromUtf8("z")) + .build())) + .setRowsLimit(chunkSize); + assertThat(nextQuery.toProto(requestContext)).isEqualTo(expectedProto.build()); + + assertThat(paginator.advance(ByteString.copyFromUtf8("c"))).isTrue(); + nextQuery = paginator.getNextQuery(); + expectedProto = + expectedReadFromAuthorizedViewProtoBuilder() .setRows( RowSet.newBuilder() .addRowRanges( @@ -407,13 +787,48 @@ public void testQueryPaginatorRangeLimitMultiplyOfChunkSize() { @Test public void testQueryPaginatorRagneNoLimit() { int chunkSize = 10; + + // Table query test. Query query = Query.create(TABLE_ID).range("a", "z"); Query.QueryPaginator paginator = query.createPaginator(chunkSize); Query nextQuery = paginator.getNextQuery(); Builder expectedProto = - expectedProtoBuilder() + expectedReadFromTableProtoBuilder() + .setRows( + RowSet.newBuilder() + .addRowRanges( + RowRange.newBuilder() + .setStartKeyClosed(ByteString.copyFromUtf8("a")) + .setEndKeyOpen(ByteString.copyFromUtf8("z")) + .build())) + .setRowsLimit(chunkSize); + assertThat(nextQuery.toProto(requestContext)).isEqualTo(expectedProto.build()); + + assertThat(paginator.advance(ByteString.copyFromUtf8("c"))).isTrue(); + nextQuery = paginator.getNextQuery(); + expectedProto + .setRows( + RowSet.newBuilder() + .addRowRanges( + RowRange.newBuilder() + .setStartKeyOpen(ByteString.copyFromUtf8("c")) + .setEndKeyOpen(ByteString.copyFromUtf8("z")) + .build())) + .setRowsLimit(chunkSize); + assertThat(nextQuery.toProto(requestContext)).isEqualTo(expectedProto.build()); + + assertThat(paginator.advance(ByteString.copyFromUtf8("z"))).isFalse(); + + // AuthorizedView query test. + query = Query.create(AuthorizedViewId.of(TABLE_ID, AUTHORIZED_VIEW_ID)).range("a", "z"); + paginator = query.createPaginator(chunkSize); + + nextQuery = paginator.getNextQuery(); + + expectedProto = + expectedReadFromAuthorizedViewProtoBuilder() .setRows( RowSet.newBuilder() .addRowRanges( @@ -443,13 +858,15 @@ public void testQueryPaginatorRagneNoLimit() { @Test public void testQueryPaginatorRowsNoLimit() { int chunkSize = 10; + + // Table query test. Query query = Query.create(TABLE_ID).rowKey("a").rowKey("b").rowKey("c"); Query.QueryPaginator paginator = query.createPaginator(chunkSize); Query nextQuery = paginator.getNextQuery(); - ReadRowsRequest.Builder expectedProto = expectedProtoBuilder(); + ReadRowsRequest.Builder expectedProto = expectedReadFromTableProtoBuilder(); expectedProto .getRowsBuilder() .addRowKeys(ByteString.copyFromUtf8("a")) @@ -461,7 +878,38 @@ public void testQueryPaginatorRowsNoLimit() { paginator.advance(ByteString.copyFromUtf8("b")); nextQuery = paginator.getNextQuery(); - expectedProto = expectedProtoBuilder(); + expectedProto = expectedReadFromTableProtoBuilder(); + expectedProto.getRowsBuilder().addRowKeys(ByteString.copyFromUtf8("c")); + expectedProto.setRowsLimit(chunkSize); + + assertThat(nextQuery.toProto(requestContext)).isEqualTo(expectedProto.build()); + + assertThat(paginator.advance(ByteString.copyFromUtf8("c"))).isFalse(); + + // AuthorizedView query test. + query = + Query.create(AuthorizedViewId.of(TABLE_ID, AUTHORIZED_VIEW_ID)) + .rowKey("a") + .rowKey("b") + .rowKey("c"); + + paginator = query.createPaginator(chunkSize); + + nextQuery = paginator.getNextQuery(); + + expectedProto = expectedReadFromAuthorizedViewProtoBuilder(); + expectedProto + .getRowsBuilder() + .addRowKeys(ByteString.copyFromUtf8("a")) + .addRowKeys(ByteString.copyFromUtf8("b")) + .addRowKeys(ByteString.copyFromUtf8("c")); + expectedProto.setRowsLimit(chunkSize); + + assertThat(nextQuery.toProto(requestContext)).isEqualTo(expectedProto.build()); + + paginator.advance(ByteString.copyFromUtf8("b")); + nextQuery = paginator.getNextQuery(); + expectedProto = expectedReadFromAuthorizedViewProtoBuilder(); expectedProto.getRowsBuilder().addRowKeys(ByteString.copyFromUtf8("c")); expectedProto.setRowsLimit(chunkSize); @@ -473,10 +921,33 @@ public void testQueryPaginatorRowsNoLimit() { @Test public void testQueryPaginatorFullTableScan() { int chunkSize = 10; + + // Table query test. Query query = Query.create(TABLE_ID); Query.QueryPaginator queryPaginator = query.createPaginator(chunkSize); - ReadRowsRequest.Builder expectedProto = expectedProtoBuilder().setRowsLimit(chunkSize); + ReadRowsRequest.Builder expectedProto = + expectedReadFromTableProtoBuilder().setRowsLimit(chunkSize); + assertThat(queryPaginator.getNextQuery().toProto(requestContext)) + .isEqualTo(expectedProto.build()); + + assertThat(queryPaginator.advance(ByteString.copyFromUtf8("a"))).isTrue(); + expectedProto + .setRows( + RowSet.newBuilder() + .addRowRanges( + RowRange.newBuilder().setStartKeyOpen(ByteString.copyFromUtf8("a")).build())) + .setRowsLimit(chunkSize); + assertThat(queryPaginator.getNextQuery().toProto(requestContext)) + .isEqualTo(expectedProto.build()); + + assertThat(queryPaginator.advance(ByteString.copyFromUtf8("a"))).isFalse(); + + // AuthorizedView query test. + query = Query.create(AuthorizedViewId.of(TABLE_ID, AUTHORIZED_VIEW_ID)); + queryPaginator = query.createPaginator(chunkSize); + + expectedProto = expectedReadFromAuthorizedViewProtoBuilder().setRowsLimit(chunkSize); assertThat(queryPaginator.getNextQuery().toProto(requestContext)) .isEqualTo(expectedProto.build()); @@ -499,7 +970,8 @@ public void testQueryPaginatorEmptyTable() { Query query = Query.create(TABLE_ID); Query.QueryPaginator queryPaginator = query.createPaginator(chunkSize); - ReadRowsRequest.Builder expectedProto = expectedProtoBuilder().setRowsLimit(chunkSize); + ReadRowsRequest.Builder expectedProto = + expectedReadFromTableProtoBuilder().setRowsLimit(chunkSize); assertThat(queryPaginator.getNextQuery().toProto(requestContext)) .isEqualTo(expectedProto.build()); @@ -510,6 +982,6 @@ public void testQueryPaginatorEmptyTable() { public void testQueryReversed() { Query query = Query.create(TABLE_ID).reversed(true); assertThat(query.toProto(requestContext)) - .isEqualTo(expectedProtoBuilder().setReversed(true).build()); + .isEqualTo(expectedReadFromTableProtoBuilder().setReversed(true).build()); } } diff --git a/google-cloud-bigtable/src/test/java/com/google/cloud/bigtable/data/v2/models/ReadModifyWriteRowTest.java b/google-cloud-bigtable/src/test/java/com/google/cloud/bigtable/data/v2/models/ReadModifyWriteRowTest.java index b0a8be33c9..90a8c6c1de 100644 --- a/google-cloud-bigtable/src/test/java/com/google/cloud/bigtable/data/v2/models/ReadModifyWriteRowTest.java +++ b/google-cloud-bigtable/src/test/java/com/google/cloud/bigtable/data/v2/models/ReadModifyWriteRowTest.java @@ -36,12 +36,14 @@ public class ReadModifyWriteRowTest { private static final String PROJECT_ID = "fake-project"; private static final String INSTANCE_ID = "fake-instance"; private static final String TABLE_ID = "fake-table"; + private static final String AUTHORIZED_VIEW_ID = "fake-authorized-view"; private static final String APP_PROFILE_ID = "fake-profile"; private static final RequestContext REQUEST_CONTEXT = RequestContext.create(PROJECT_ID, INSTANCE_ID, APP_PROFILE_ID); @Test public void testAppend() { + // Test ReadModifyWriteRow on a table. ReadModifyWriteRow mutation = ReadModifyWriteRow.create(TABLE_ID, "fake-key") .append( @@ -69,10 +71,42 @@ public void testAppend() { .setAppendValue(ByteString.copyFromUtf8("fake-value-str"))) .build(); assertThat(actualProto).isEqualTo(expected); + + // Test ReadModifyWriteRow on an authorized view. + mutation = + ReadModifyWriteRow.create(AuthorizedViewId.of(TABLE_ID, AUTHORIZED_VIEW_ID), "fake-key") + .append( + "fake-family", + ByteString.copyFromUtf8("fake-qualifier"), + ByteString.copyFromUtf8("fake-value")) + .append("fake-family", "fake-qualifier-str", "fake-value-str"); + + actualProto = mutation.toProto(REQUEST_CONTEXT); + + expected = + ReadModifyWriteRowRequest.newBuilder() + .setAuthorizedViewName( + NameUtil.formatAuthorizedViewName( + PROJECT_ID, INSTANCE_ID, TABLE_ID, AUTHORIZED_VIEW_ID)) + .setAppProfileId(APP_PROFILE_ID) + .setRowKey(ByteString.copyFromUtf8("fake-key")) + .addRules( + ReadModifyWriteRule.newBuilder() + .setFamilyName("fake-family") + .setColumnQualifier(ByteString.copyFromUtf8("fake-qualifier")) + .setAppendValue(ByteString.copyFromUtf8("fake-value"))) + .addRules( + ReadModifyWriteRule.newBuilder() + .setFamilyName("fake-family") + .setColumnQualifier(ByteString.copyFromUtf8("fake-qualifier-str")) + .setAppendValue(ByteString.copyFromUtf8("fake-value-str"))) + .build(); + assertThat(actualProto).isEqualTo(expected); } @Test public void testIncrement() { + // Test ReadModifyWriteRow on a table. ReadModifyWriteRow mutation = ReadModifyWriteRow.create(TABLE_ID, "fake-key") .increment("fake-family", ByteString.copyFromUtf8("fake-qualifier"), 1) @@ -97,10 +131,39 @@ public void testIncrement() { .setColumnQualifier(ByteString.copyFromUtf8("fake-qualifier-str")) .setIncrementAmount(2)) .build()); + + // Test ReadModifyWriteRow on an authorized view. + mutation = + ReadModifyWriteRow.create(AuthorizedViewId.of(TABLE_ID, AUTHORIZED_VIEW_ID), "fake-key") + .increment("fake-family", ByteString.copyFromUtf8("fake-qualifier"), 1) + .increment("fake-family", "fake-qualifier-str", 2); + + actualProto = mutation.toProto(REQUEST_CONTEXT); + + assertThat(actualProto) + .isEqualTo( + ReadModifyWriteRowRequest.newBuilder() + .setAuthorizedViewName( + NameUtil.formatAuthorizedViewName( + PROJECT_ID, INSTANCE_ID, TABLE_ID, AUTHORIZED_VIEW_ID)) + .setAppProfileId(APP_PROFILE_ID) + .setRowKey(ByteString.copyFromUtf8("fake-key")) + .addRules( + ReadModifyWriteRule.newBuilder() + .setFamilyName("fake-family") + .setColumnQualifier(ByteString.copyFromUtf8("fake-qualifier")) + .setIncrementAmount(1)) + .addRules( + ReadModifyWriteRule.newBuilder() + .setFamilyName("fake-family") + .setColumnQualifier(ByteString.copyFromUtf8("fake-qualifier-str")) + .setIncrementAmount(2)) + .build()); } @Test public void serializationTest() throws IOException, ClassNotFoundException { + // Test ReadModifyWriteRow on a table. ReadModifyWriteRow expected = ReadModifyWriteRow.create(TABLE_ID, "fake-key") .increment("fake-family", ByteString.copyFromUtf8("fake-qualifier"), 1) @@ -115,10 +178,27 @@ public void serializationTest() throws IOException, ClassNotFoundException { ReadModifyWriteRow actual = (ReadModifyWriteRow) ois.readObject(); assertThat(actual.toProto(REQUEST_CONTEXT)).isEqualTo(expected.toProto(REQUEST_CONTEXT)); + + // Test ReadModifyWriteRow on an authorized view. + expected = + ReadModifyWriteRow.create(AuthorizedViewId.of(TABLE_ID, AUTHORIZED_VIEW_ID), "fake-key") + .increment("fake-family", ByteString.copyFromUtf8("fake-qualifier"), 1) + .append("fake-family", "a", "b"); + + bos = new ByteArrayOutputStream(); + oos = new ObjectOutputStream(bos); + oos.writeObject(expected); + oos.close(); + + ois = new ObjectInputStream(new ByteArrayInputStream(bos.toByteArray())); + + actual = (ReadModifyWriteRow) ois.readObject(); + assertThat(actual.toProto(REQUEST_CONTEXT)).isEqualTo(expected.toProto(REQUEST_CONTEXT)); } @Test public void fromProtoTest() { + // Test ReadModifyWriteRow on a table. ReadModifyWriteRow expected = ReadModifyWriteRow.create(TABLE_ID, "row-key") .increment("fake-family", ByteString.copyFromUtf8("fake-qualifier"), 1) @@ -138,6 +218,28 @@ public void fromProtoTest() { assertThat(overriddenRequest).isNotEqualTo(protoRequest); assertThat(overriddenRequest.getTableName()) .matches(NameUtil.formatTableName(projectId, instanceId, TABLE_ID)); + assertThat(overriddenRequest.getAuthorizedViewName()).isEmpty(); + assertThat(overriddenRequest.getAppProfileId()).matches(appProfile); + + // Test ReadModifyWriteRow on an authorized view. + expected = + ReadModifyWriteRow.create(AuthorizedViewId.of(TABLE_ID, AUTHORIZED_VIEW_ID), "row-key") + .increment("fake-family", ByteString.copyFromUtf8("fake-qualifier"), 1) + .append("fake-family", "fake-qualifier", "fake-value"); + + protoRequest = expected.toProto(REQUEST_CONTEXT); + actualRequest = ReadModifyWriteRow.fromProto(protoRequest); + + assertThat(actualRequest.toProto(REQUEST_CONTEXT)).isEqualTo(protoRequest); + + overriddenRequest = + actualRequest.toProto(RequestContext.create(projectId, instanceId, appProfile)); + + assertThat(overriddenRequest).isNotEqualTo(protoRequest); + assertThat(overriddenRequest.getTableName()).isEmpty(); + assertThat(overriddenRequest.getAuthorizedViewName()) + .matches( + NameUtil.formatAuthorizedViewName(projectId, instanceId, TABLE_ID, AUTHORIZED_VIEW_ID)); assertThat(overriddenRequest.getAppProfileId()).matches(appProfile); } } diff --git a/google-cloud-bigtable/src/test/java/com/google/cloud/bigtable/data/v2/models/RowMutationTest.java b/google-cloud-bigtable/src/test/java/com/google/cloud/bigtable/data/v2/models/RowMutationTest.java index b401ad5ef3..2e59c56336 100644 --- a/google-cloud-bigtable/src/test/java/com/google/cloud/bigtable/data/v2/models/RowMutationTest.java +++ b/google-cloud-bigtable/src/test/java/com/google/cloud/bigtable/data/v2/models/RowMutationTest.java @@ -38,16 +38,19 @@ public class RowMutationTest { private static final String PROJECT_ID = "fake-project"; private static final String INSTANCE_ID = "fake-instance"; private static final String TABLE_ID = "fake-table"; + private static final String AUTHORIZED_VIEW_ID = "fake-authorized-view"; private static final String APP_PROFILE_ID = "fake-profile"; private static final RequestContext REQUEST_CONTEXT = RequestContext.create(PROJECT_ID, INSTANCE_ID, APP_PROFILE_ID); + private static final ByteString TEST_KEY = ByteString.copyFromUtf8("fake-key"); @Test public void toProtoTest() { long timestampMin = System.currentTimeMillis() * 1_000; + // Test RowMutation on a table. RowMutation rowMutation = - RowMutation.create("fake-table", "fake-key") + RowMutation.create(TABLE_ID, TEST_KEY) .setCell("fake-family", "fake-qualifier", "fake-value"); MutateRowRequest actualRowMutation = rowMutation.toProto(REQUEST_CONTEXT); @@ -55,7 +58,29 @@ public void toProtoTest() { com.google.common.collect.Range.closed(timestampMin, System.currentTimeMillis() * 1_000); assertThat(actualRowMutation.getTableName()) - .isEqualTo(NameUtil.formatTableName(PROJECT_ID, INSTANCE_ID, "fake-table")); + .isEqualTo(NameUtil.formatTableName(PROJECT_ID, INSTANCE_ID, TABLE_ID)); + assertThat(actualRowMutation.getAuthorizedViewName()).isEmpty(); + assertThat(actualRowMutation.getAppProfileId()).isEqualTo(APP_PROFILE_ID); + assertThat(actualRowMutation.getMutationsList()).hasSize(1); + assertThat(actualRowMutation.getMutations(0).getSetCell().getValue()) + .isEqualTo(ByteString.copyFromUtf8("fake-value")); + assertThat(actualRowMutation.getMutations(0).getSetCell().getTimestampMicros()) + .isIn(timestampRange); + + // Test RowMutation on an authorized view. + rowMutation = + RowMutation.create(AuthorizedViewId.of(TABLE_ID, AUTHORIZED_VIEW_ID), TEST_KEY) + .setCell("fake-family", "fake-qualifier", "fake-value"); + + actualRowMutation = rowMutation.toProto(REQUEST_CONTEXT); + timestampRange = + com.google.common.collect.Range.closed(timestampMin, System.currentTimeMillis() * 1_000); + + assertThat(actualRowMutation.getTableName()).isEmpty(); + assertThat(actualRowMutation.getAuthorizedViewName()) + .isEqualTo( + NameUtil.formatAuthorizedViewName( + PROJECT_ID, INSTANCE_ID, TABLE_ID, AUTHORIZED_VIEW_ID)); assertThat(actualRowMutation.getAppProfileId()).isEqualTo(APP_PROFILE_ID); assertThat(actualRowMutation.getMutationsList()).hasSize(1); assertThat(actualRowMutation.getMutations(0).getSetCell().getValue()) @@ -68,8 +93,9 @@ public void toProtoTest() { public void toBulkProtoTest() { long timestampMin = System.currentTimeMillis() * 1_000; + // Test RowMutation on a table. RowMutation rowMutation = - RowMutation.create("fake-table", "fake-key") + RowMutation.create(TABLE_ID, TEST_KEY) .setCell("fake-family", "fake-qualifier", "fake-value"); MutateRowsRequest actualRowMutation = rowMutation.toBulkProto(REQUEST_CONTEXT); @@ -79,6 +105,31 @@ public void toBulkProtoTest() { assertThat(actualRowMutation.getTableName()) .isEqualTo(NameUtil.formatTableName(PROJECT_ID, INSTANCE_ID, TABLE_ID)); + assertThat(actualRowMutation.getAuthorizedViewName()).isEmpty(); + assertThat(actualRowMutation.getAppProfileId()).isEqualTo(APP_PROFILE_ID); + assertThat(actualRowMutation.getEntriesList()).hasSize(1); + assertThat(actualRowMutation.getEntries(0).getMutationsList()).hasSize(1); + assertThat(actualRowMutation.getEntries(0).getMutations(0).getSetCell().getValue()) + .isEqualTo(ByteString.copyFromUtf8("fake-value")); + + assertThat(actualRowMutation.getEntries(0).getMutations(0).getSetCell().getTimestampMicros()) + .isIn(timestampRange); + + // Test RowMutation on an authorized view. + rowMutation = + RowMutation.create(AuthorizedViewId.of(TABLE_ID, AUTHORIZED_VIEW_ID), TEST_KEY) + .setCell("fake-family", "fake-qualifier", "fake-value"); + + actualRowMutation = rowMutation.toBulkProto(REQUEST_CONTEXT); + + timestampRange = + com.google.common.collect.Range.closed(timestampMin, System.currentTimeMillis() * 1_000); + + assertThat(actualRowMutation.getTableName()).isEmpty(); + assertThat(actualRowMutation.getAuthorizedViewName()) + .isEqualTo( + NameUtil.formatAuthorizedViewName( + PROJECT_ID, INSTANCE_ID, TABLE_ID, AUTHORIZED_VIEW_ID)); assertThat(actualRowMutation.getAppProfileId()).isEqualTo(APP_PROFILE_ID); assertThat(actualRowMutation.getEntriesList()).hasSize(1); assertThat(actualRowMutation.getEntries(0).getMutationsList()).hasSize(1); @@ -92,17 +143,27 @@ public void toBulkProtoTest() { @Test public void toProtoTestWithProvidedMutation() { Mutation mutation = Mutation.create().setCell("fake-family", "fake-qualifier", "fake-value"); - RowMutation rowMutation = RowMutation.create("fake-table", "fake-key", mutation); + // Test RowMutation on a table. + RowMutation rowMutation = RowMutation.create(TABLE_ID, TEST_KEY, mutation); MutateRowRequest actualRowMutation = rowMutation.toProto(REQUEST_CONTEXT); assertThat(actualRowMutation.getMutationsList()).isEqualTo(mutation.getMutations()); + + // Test RowMutation on an authorized view. + rowMutation = + RowMutation.create(AuthorizedViewId.of(TABLE_ID, AUTHORIZED_VIEW_ID), TEST_KEY, mutation); + + actualRowMutation = rowMutation.toProto(REQUEST_CONTEXT); + + assertThat(actualRowMutation.getMutationsList()).isEqualTo(mutation.getMutations()); } @Test public void serializationTest() throws IOException, ClassNotFoundException { + // Test RowMutation on a table. RowMutation expected = - RowMutation.create("fake-table", "fake-key") + RowMutation.create(TABLE_ID, TEST_KEY) .setCell("fake-family", "fake-qualifier", 10_000, "fake-value"); ByteArrayOutputStream bos = new ByteArrayOutputStream(); @@ -114,12 +175,28 @@ public void serializationTest() throws IOException, ClassNotFoundException { RowMutation actual = (RowMutation) ois.readObject(); assertThat(actual.toProto(REQUEST_CONTEXT)).isEqualTo(expected.toProto(REQUEST_CONTEXT)); + + // Test RowMutation on an authorized view. + expected = + RowMutation.create(AuthorizedViewId.of(TABLE_ID, AUTHORIZED_VIEW_ID), TEST_KEY) + .setCell("fake-family", "fake-qualifier", 10_000, "fake-value"); + + bos = new ByteArrayOutputStream(); + oos = new ObjectOutputStream(bos); + oos.writeObject(expected); + oos.close(); + + ois = new ObjectInputStream(new ByteArrayInputStream(bos.toByteArray())); + + actual = (RowMutation) ois.readObject(); + assertThat(actual.toProto(REQUEST_CONTEXT)).isEqualTo(expected.toProto(REQUEST_CONTEXT)); } @Test public void testWithLongValue() { + // Test RowMutation on a table. RowMutation rowMutation = - RowMutation.create("fake-table", "fake-key") + RowMutation.create(TABLE_ID, TEST_KEY) .setCell("fake-family", "fake-qualifier", 100_000L) .setCell("fake-family", "fake-qualifier", 30_000L, 100_000L); @@ -130,6 +207,28 @@ public void testWithLongValue() { assertThat(setCell.getColumnQualifier().toStringUtf8()).isEqualTo("fake-qualifier"); assertThat(setCell.getValue()).isEqualTo(ByteString.copyFrom(Longs.toByteArray(100_000L))); + assertThat(actualRowMutation.getMutations(1).getSetCell()) + .isEqualTo( + SetCell.newBuilder() + .setFamilyName("fake-family") + .setColumnQualifier(ByteString.copyFromUtf8("fake-qualifier")) + .setTimestampMicros(30_000L) + .setValue(ByteString.copyFrom(Longs.toByteArray(100_000L))) + .build()); + + // Test RowMutation on an authorized view. + rowMutation = + RowMutation.create(AuthorizedViewId.of(TABLE_ID, AUTHORIZED_VIEW_ID), TEST_KEY) + .setCell("fake-family", "fake-qualifier", 100_000L) + .setCell("fake-family", "fake-qualifier", 30_000L, 100_000L); + + actualRowMutation = rowMutation.toProto(REQUEST_CONTEXT); + + setCell = actualRowMutation.getMutations(0).getSetCell(); + assertThat(setCell.getFamilyName()).isEqualTo("fake-family"); + assertThat(setCell.getColumnQualifier().toStringUtf8()).isEqualTo("fake-qualifier"); + assertThat(setCell.getValue()).isEqualTo(ByteString.copyFrom(Longs.toByteArray(100_000L))); + assertThat(actualRowMutation.getMutations(1).getSetCell()) .isEqualTo( SetCell.newBuilder() @@ -142,8 +241,9 @@ public void testWithLongValue() { @Test public void fromProtoTest() { + // Test RowMutation on a table. RowMutation rowMutation = - RowMutation.create("fake-table", "fake-key") + RowMutation.create(TABLE_ID, TEST_KEY) .setCell("fake-family", "fake-qualifier-1", "fake-value") .setCell("fake-family", "fake-qualifier-2", 30_000L, "fake-value-2"); @@ -161,6 +261,28 @@ public void fromProtoTest() { assertThat(overriddenRequest).isNotEqualTo(protoRequest); assertThat(overriddenRequest.getTableName()) .matches(NameUtil.formatTableName(projectId, instanceId, TABLE_ID)); + assertThat(overriddenRequest.getAuthorizedViewName()).isEmpty(); + assertThat(overriddenRequest.getAppProfileId()).matches(appProfile); + + // Test RowMutation on an authorized view. + rowMutation = + RowMutation.create(AuthorizedViewId.of(TABLE_ID, AUTHORIZED_VIEW_ID), TEST_KEY) + .setCell("fake-family", "fake-qualifier-1", "fake-value") + .setCell("fake-family", "fake-qualifier-2", 30_000L, "fake-value-2"); + + protoRequest = rowMutation.toProto(REQUEST_CONTEXT); + actualRequest = RowMutation.fromProto(protoRequest); + + assertThat(actualRequest.toProto(REQUEST_CONTEXT)).isEqualTo(protoRequest); + + overriddenRequest = + actualRequest.toProto(RequestContext.create(projectId, instanceId, appProfile)); + + assertThat(overriddenRequest).isNotEqualTo(protoRequest); + assertThat(overriddenRequest.getTableName()).isEmpty(); + assertThat(overriddenRequest.getAuthorizedViewName()) + .matches( + NameUtil.formatAuthorizedViewName(projectId, instanceId, TABLE_ID, AUTHORIZED_VIEW_ID)); assertThat(overriddenRequest.getAppProfileId()).matches(appProfile); } } diff --git a/google-cloud-bigtable/src/test/java/com/google/cloud/bigtable/data/v2/models/SampleRowKeysRequestTest.java b/google-cloud-bigtable/src/test/java/com/google/cloud/bigtable/data/v2/models/SampleRowKeysRequestTest.java new file mode 100644 index 0000000000..4aa8a2b809 --- /dev/null +++ b/google-cloud-bigtable/src/test/java/com/google/cloud/bigtable/data/v2/models/SampleRowKeysRequestTest.java @@ -0,0 +1,178 @@ +/* + * Copyright 2024 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.models; + +import static com.google.common.truth.Truth.assertThat; + +import com.google.cloud.bigtable.data.v2.internal.NameUtil; +import com.google.cloud.bigtable.data.v2.internal.RequestContext; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.ExpectedException; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + +@RunWith(JUnit4.class) +public class SampleRowKeysRequestTest { + private static final String PROJECT_ID = "fake-project"; + private static final String INSTANCE_ID = "fake-instance"; + private static final String TABLE_ID = "fake-table"; + private static final String AUTHORIZED_VIEW_ID = "fake-authorized-view"; + private static final String APP_PROFILE_ID = "fake-profile"; + private static final RequestContext REQUEST_CONTEXT = + RequestContext.create(PROJECT_ID, INSTANCE_ID, APP_PROFILE_ID); + @Rule public ExpectedException expect = ExpectedException.none(); + + @Test + public void toProtoTest() { + // Test SampleRowKeysRequest on a table. + SampleRowKeysRequest sampleRowKeysRequest = SampleRowKeysRequest.create(TableId.of(TABLE_ID)); + com.google.bigtable.v2.SampleRowKeysRequest actualRequest = + sampleRowKeysRequest.toProto(REQUEST_CONTEXT); + assertThat(actualRequest.getTableName()) + .isEqualTo(NameUtil.formatTableName(PROJECT_ID, INSTANCE_ID, TABLE_ID)); + assertThat(actualRequest.getAuthorizedViewName()).isEmpty(); + assertThat(actualRequest.getAppProfileId()).isEqualTo(APP_PROFILE_ID); + + // Test SampleRowKeysRequest on an authorized view. + sampleRowKeysRequest = + SampleRowKeysRequest.create(AuthorizedViewId.of(TABLE_ID, AUTHORIZED_VIEW_ID)); + actualRequest = sampleRowKeysRequest.toProto(REQUEST_CONTEXT); + assertThat(actualRequest.getTableName()).isEmpty(); + assertThat(actualRequest.getAuthorizedViewName()) + .isEqualTo( + NameUtil.formatAuthorizedViewName( + PROJECT_ID, INSTANCE_ID, TABLE_ID, AUTHORIZED_VIEW_ID)); + assertThat(actualRequest.getAppProfileId()).isEqualTo(APP_PROFILE_ID); + } + + @Test + public void fromProtoTest() { + // Test SampleRowKeysRequest on a table. + SampleRowKeysRequest sampleRowKeysRequest = SampleRowKeysRequest.create(TableId.of(TABLE_ID)); + + com.google.bigtable.v2.SampleRowKeysRequest protoRequest = + sampleRowKeysRequest.toProto(REQUEST_CONTEXT); + SampleRowKeysRequest actualRequest = SampleRowKeysRequest.fromProto(protoRequest); + + assertThat(actualRequest.toProto(REQUEST_CONTEXT)).isEqualTo(protoRequest); + + String projectId = "fresh-project"; + String instanceId = "fresh-instance"; + String appProfile = "fresh-app-profile"; + com.google.bigtable.v2.SampleRowKeysRequest overriddenRequest = + actualRequest.toProto(RequestContext.create(projectId, instanceId, appProfile)); + + assertThat(overriddenRequest).isNotEqualTo(protoRequest); + assertThat(overriddenRequest.getTableName()) + .matches(NameUtil.formatTableName(projectId, instanceId, TABLE_ID)); + assertThat(overriddenRequest.getAuthorizedViewName()).isEmpty(); + assertThat(overriddenRequest.getAppProfileId()).matches(appProfile); + + // Test SampleRowKeysRequest on an authorized view. + sampleRowKeysRequest = + SampleRowKeysRequest.create(AuthorizedViewId.of(TABLE_ID, AUTHORIZED_VIEW_ID)); + + protoRequest = sampleRowKeysRequest.toProto(REQUEST_CONTEXT); + actualRequest = SampleRowKeysRequest.fromProto(protoRequest); + + assertThat(actualRequest.toProto(REQUEST_CONTEXT)).isEqualTo(protoRequest); + + overriddenRequest = + actualRequest.toProto(RequestContext.create(projectId, instanceId, appProfile)); + + assertThat(overriddenRequest).isNotEqualTo(protoRequest); + assertThat(overriddenRequest.getTableName()).isEmpty(); + assertThat(overriddenRequest.getAuthorizedViewName()) + .matches( + NameUtil.formatAuthorizedViewName(projectId, instanceId, TABLE_ID, AUTHORIZED_VIEW_ID)); + assertThat(overriddenRequest.getAppProfileId()).matches(appProfile); + } + + @Test(expected = IllegalArgumentException.class) + public void testFromProtoWithInvalidTableId() { + SampleRowKeysRequest.fromProto( + com.google.bigtable.v2.SampleRowKeysRequest.getDefaultInstance() + .toBuilder() + .setTableName("invalid-name") + .build()); + + expect.expect(IllegalArgumentException.class); + expect.expectMessage("Invalid table name:"); + } + + @Test(expected = IllegalArgumentException.class) + public void testFromProtoWithInvalidAuthorizedViewId() { + SampleRowKeysRequest.fromProto( + com.google.bigtable.v2.SampleRowKeysRequest.getDefaultInstance() + .toBuilder() + .setAuthorizedViewName("invalid-name") + .build()); + + expect.expect(IllegalArgumentException.class); + expect.expectMessage("Invalid authorized view name:"); + } + + @Test(expected = IllegalArgumentException.class) + public void testFromProtoWithEmptyTableAndAuthorizedViewId() { + SampleRowKeysRequest.fromProto( + com.google.bigtable.v2.SampleRowKeysRequest.getDefaultInstance()); + + expect.expect(IllegalArgumentException.class); + expect.expectMessage("Either table name or authorized view name must be specified"); + } + + @Test(expected = IllegalArgumentException.class) + public void testFromProtoWithBothTableAndAuthorizedViewId() { + SampleRowKeysRequest.fromProto( + com.google.bigtable.v2.SampleRowKeysRequest.getDefaultInstance() + .toBuilder() + .setTableName(NameUtil.formatTableName(PROJECT_ID, INSTANCE_ID, TABLE_ID)) + .setAuthorizedViewName( + NameUtil.formatAuthorizedViewName( + PROJECT_ID, INSTANCE_ID, TABLE_ID, AUTHORIZED_VIEW_ID)) + .build()); + + expect.expect(IllegalArgumentException.class); + expect.expectMessage( + "Table name and authorized view name cannot be specified at the same time"); + } + + @Test + public void testEquality() { + // Test SampleRowKeysRequest on a table. + assertThat(SampleRowKeysRequest.create(TableId.of(TABLE_ID))) + .isEqualTo(SampleRowKeysRequest.create(TableId.of(TABLE_ID))); + assertThat(SampleRowKeysRequest.create(TableId.of("another-table"))) + .isNotEqualTo(SampleRowKeysRequest.create(TableId.of(TABLE_ID))); + + // Test SampleRowKeysRequest on an authorized view. + assertThat(SampleRowKeysRequest.create(AuthorizedViewId.of(TABLE_ID, AUTHORIZED_VIEW_ID))) + .isEqualTo(SampleRowKeysRequest.create(AuthorizedViewId.of(TABLE_ID, AUTHORIZED_VIEW_ID))); + assertThat( + SampleRowKeysRequest.create(AuthorizedViewId.of("another-table", AUTHORIZED_VIEW_ID))) + .isNotEqualTo( + SampleRowKeysRequest.create(AuthorizedViewId.of(TABLE_ID, AUTHORIZED_VIEW_ID))); + assertThat( + SampleRowKeysRequest.create(AuthorizedViewId.of(TABLE_ID, "another-authorized-view"))) + .isNotEqualTo( + SampleRowKeysRequest.create(AuthorizedViewId.of(TABLE_ID, AUTHORIZED_VIEW_ID))); + + assertThat(SampleRowKeysRequest.create(AuthorizedViewId.of(TABLE_ID, AUTHORIZED_VIEW_ID))) + .isNotEqualTo(SampleRowKeysRequest.create(TableId.of(TABLE_ID))); + } +} diff --git a/google-cloud-bigtable/src/test/java/com/google/cloud/bigtable/data/v2/models/TableIdTest.java b/google-cloud-bigtable/src/test/java/com/google/cloud/bigtable/data/v2/models/TableIdTest.java new file mode 100644 index 0000000000..8e1c9d502d --- /dev/null +++ b/google-cloud-bigtable/src/test/java/com/google/cloud/bigtable/data/v2/models/TableIdTest.java @@ -0,0 +1,61 @@ +/* + * Copyright 2024 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.models; + +import static com.google.common.truth.Truth.assertThat; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + +@RunWith(JUnit4.class) +public class TableIdTest { + private static final String PROJECT_ID = "my-project"; + private static final String INSTANCE_ID = "my-instance"; + private static final String TABLE_ID = "my-table"; + + @Test + public void testToResourceName() { + TableId tableId = TableId.of(TABLE_ID); + + assertThat(tableId.toResourceName(PROJECT_ID, INSTANCE_ID)) + .isEqualTo("projects/my-project/instances/my-instance/tables/my-table"); + } + + @Test + public void testEquality() { + TableId tableId = TableId.of(TABLE_ID); + + assertThat(tableId).isEqualTo(TableId.of(TABLE_ID)); + assertThat(tableId).isNotEqualTo(TableId.of("another-table")); + } + + @Test + public void testHashCode() { + TableId tableId = TableId.of(TABLE_ID); + + assertThat(tableId.hashCode()).isEqualTo(TableId.of(TABLE_ID).hashCode()); + assertThat(tableId.hashCode()).isNotEqualTo(TableId.of("another-table").hashCode()); + } + + @Test + public void testToString() { + TableId tableId = TableId.of(TABLE_ID); + + assertThat(tableId.toString()).isEqualTo("TableId{tableId=my-table}"); + } +} diff --git a/google-cloud-bigtable/src/test/java/com/google/cloud/bigtable/data/v2/stub/EnhancedBigtableStubTest.java b/google-cloud-bigtable/src/test/java/com/google/cloud/bigtable/data/v2/stub/EnhancedBigtableStubTest.java index eacf145bcb..2eb0700488 100644 --- a/google-cloud-bigtable/src/test/java/com/google/cloud/bigtable/data/v2/stub/EnhancedBigtableStubTest.java +++ b/google-cloud-bigtable/src/test/java/com/google/cloud/bigtable/data/v2/stub/EnhancedBigtableStubTest.java @@ -16,11 +16,13 @@ package com.google.cloud.bigtable.data.v2.stub; import static com.google.common.truth.Truth.assertThat; +import static org.junit.Assert.assertThrows; import com.google.api.client.json.gson.GsonFactory; import com.google.api.client.json.webtoken.JsonWebSignature; import com.google.api.gax.batching.Batcher; import com.google.api.gax.batching.BatcherImpl; +import com.google.api.gax.batching.BatchingException; import com.google.api.gax.batching.BatchingSettings; import com.google.api.gax.batching.FlowControlSettings; import com.google.api.gax.batching.FlowController.LimitExceededBehavior; @@ -51,13 +53,14 @@ import com.google.cloud.bigtable.data.v2.FakeServiceBuilder; import com.google.cloud.bigtable.data.v2.internal.RequestContext; import com.google.cloud.bigtable.data.v2.models.*; -import com.google.cloud.bigtable.data.v2.models.Row; import com.google.common.collect.ImmutableMap; import com.google.common.collect.Queues; import com.google.common.io.BaseEncoding; import com.google.protobuf.ByteString; import com.google.protobuf.BytesValue; import com.google.protobuf.StringValue; +import com.google.rpc.Code; +import com.google.rpc.Status; import io.grpc.Context; import io.grpc.Deadline; import io.grpc.ManagedChannel; @@ -94,6 +97,7 @@ 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) @@ -118,7 +122,7 @@ public class EnhancedBigtableStubTest { public void setUp() throws IOException, IllegalAccessException, InstantiationException { metadataInterceptor = new MetadataInterceptor(); contextInterceptor = new ContextInterceptor(); - fakeDataService = new FakeDataService(); + fakeDataService = Mockito.spy(new FakeDataService()); server = FakeServiceBuilder.create(fakeDataService) @@ -592,6 +596,69 @@ public void testReadChangeStreamWaitTimeoutIsSet() throws Exception { } } + @Test + public void testBatchMutationsPartialFailure() { + Batcher batcher = + enhancedBigtableStub.newMutateRowsBatcher("table1", GrpcCallContext.createDefault()); + + batcher.add(RowMutationEntry.create("key0").deleteRow()); + batcher.add(RowMutationEntry.create("key1").deleteRow()); + + Mockito.doAnswer( + invocationOnMock -> { + StreamObserver observer = invocationOnMock.getArgument(1); + observer.onNext( + MutateRowsResponse.newBuilder() + .addEntries( + MutateRowsResponse.Entry.newBuilder() + .setIndex(0) + .setStatus(Status.newBuilder().setCode(Code.OK_VALUE)) + .build()) + .addEntries( + MutateRowsResponse.Entry.newBuilder() + .setIndex(1) + .setStatus( + Status.newBuilder() + .setCode(Code.PERMISSION_DENIED_VALUE) + .setMessage("fake partial error")) + .build()) + .build()); + observer.onCompleted(); + return null; + }) + .when(fakeDataService) + .mutateRows(Mockito.any(MutateRowsRequest.class), Mockito.any(StreamObserver.class)); + BatchingException batchingException = + assertThrows(BatchingException.class, () -> batcher.close()); + assertThat(batchingException.getMessage()) + .contains( + "Batching finished with 1 partial failures. The 1 partial failures contained 1 entries that failed with: 1 ApiException(1 PERMISSION_DENIED)."); + assertThat(batchingException.getMessage()).contains("fake partial error"); + assertThat(batchingException.getMessage()).doesNotContain("INTERNAL"); + } + + @Test + public void testBatchMutationRPCErrorCode() { + Batcher batcher = + enhancedBigtableStub.newMutateRowsBatcher("table1", GrpcCallContext.createDefault()); + + Mockito.doAnswer( + invocationOnMock -> { + StreamObserver observer = invocationOnMock.getArgument(1); + observer.onError(io.grpc.Status.PERMISSION_DENIED.asException()); + return null; + }) + .when(fakeDataService) + .mutateRows(Mockito.any(MutateRowsRequest.class), Mockito.any(StreamObserver.class)); + + batcher.add(RowMutationEntry.create("key0").deleteRow()); + BatchingException batchingException = + assertThrows(BatchingException.class, () -> batcher.close()); + assertThat(batchingException.getMessage()) + .contains( + "Batching finished with 1 batches failed to apply due to: 1 ApiException(1 PERMISSION_DENIED) and 0 partial failures"); + } + private static class MetadataInterceptor implements ServerInterceptor { final BlockingQueue headers = Queues.newLinkedBlockingDeque(); diff --git a/google-cloud-bigtable/src/test/java/com/google/cloud/bigtable/data/v2/stub/SampleRowKeysCallableWithRequestTest.java b/google-cloud-bigtable/src/test/java/com/google/cloud/bigtable/data/v2/stub/SampleRowKeysCallableWithRequestTest.java new file mode 100644 index 0000000000..f974076ceb --- /dev/null +++ b/google-cloud-bigtable/src/test/java/com/google/cloud/bigtable/data/v2/stub/SampleRowKeysCallableWithRequestTest.java @@ -0,0 +1,129 @@ +/* + * Copyright 2024 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.stub; + +import static com.google.common.truth.Truth.assertThat; + +import com.google.api.core.ApiFuture; +import com.google.api.core.SettableApiFuture; +import com.google.api.gax.grpc.GrpcStatusCode; +import com.google.api.gax.rpc.ApiCallContext; +import com.google.api.gax.rpc.NotFoundException; +import com.google.api.gax.rpc.UnaryCallable; +import com.google.bigtable.v2.SampleRowKeysResponse; +import com.google.cloud.bigtable.data.v2.internal.NameUtil; +import com.google.cloud.bigtable.data.v2.internal.RequestContext; +import com.google.cloud.bigtable.data.v2.models.KeyOffset; +import com.google.cloud.bigtable.data.v2.models.SampleRowKeysRequest; +import com.google.cloud.bigtable.data.v2.models.TableId; +import com.google.common.collect.ImmutableList; +import com.google.protobuf.ByteString; +import io.grpc.Status.Code; +import java.util.List; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.TimeUnit; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + +@RunWith(JUnit4.class) +public class SampleRowKeysCallableWithRequestTest { + + private final RequestContext requestContext = + RequestContext.create("my-project", "my-instance", "my-profile"); + private FakeCallable inner; + private SampleRowKeysCallableWithRequest callable; + + @Before + public void setUp() { + inner = new FakeCallable(); + callable = new SampleRowKeysCallableWithRequest(inner, requestContext); + } + + @Test + public void requestIsCorrect() { + callable.futureCall(SampleRowKeysRequest.create(TableId.of("my-table"))); + + assertThat(inner.request) + .isEqualTo( + com.google.bigtable.v2.SampleRowKeysRequest.newBuilder() + .setTableName( + NameUtil.formatTableName( + requestContext.getProjectId(), requestContext.getInstanceId(), "my-table")) + .setAppProfileId(requestContext.getAppProfileId()) + .build()); + } + + @Test + public void responseCorrectlyTransformed() throws Exception { + ApiFuture> result = + callable.futureCall(SampleRowKeysRequest.create(TableId.of("my-table"))); + + inner.response.set( + ImmutableList.of( + SampleRowKeysResponse.newBuilder() + .setRowKey(ByteString.copyFromUtf8("key1")) + .setOffsetBytes(100) + .build(), + SampleRowKeysResponse.newBuilder() + .setRowKey(ByteString.copyFromUtf8("")) + .setOffsetBytes(1000) + .build())); + + assertThat(result.get(1, TimeUnit.SECONDS)) + .isEqualTo( + ImmutableList.of( + KeyOffset.create(ByteString.copyFromUtf8("key1"), 100), + KeyOffset.create(ByteString.EMPTY, 1000))); + } + + @Test + public void errorIsPropagated() throws Exception { + ApiFuture> result = + callable.futureCall(SampleRowKeysRequest.create(TableId.of("my-table"))); + + Throwable expectedError = + new NotFoundException("fake error", null, GrpcStatusCode.of(Code.NOT_FOUND), false); + inner.response.setException(expectedError); + + Throwable actualError = null; + try { + result.get(1, TimeUnit.SECONDS); + } catch (ExecutionException e) { + actualError = e.getCause(); + } + + assertThat(actualError).isEqualTo(expectedError); + } + + static class FakeCallable + extends UnaryCallable< + com.google.bigtable.v2.SampleRowKeysRequest, List> { + com.google.bigtable.v2.SampleRowKeysRequest request; + ApiCallContext callContext; + SettableApiFuture> response = SettableApiFuture.create(); + + @Override + public ApiFuture> futureCall( + com.google.bigtable.v2.SampleRowKeysRequest request, ApiCallContext context) { + this.request = request; + this.callContext = context; + + return response; + } + } +} diff --git a/google-cloud-bigtable/src/test/java/com/google/cloud/bigtable/data/v2/stub/metrics/BigtableTracerCallableTest.java b/google-cloud-bigtable/src/test/java/com/google/cloud/bigtable/data/v2/stub/metrics/BigtableTracerCallableTest.java index 527e41e046..5d16b623fd 100644 --- a/google-cloud-bigtable/src/test/java/com/google/cloud/bigtable/data/v2/stub/metrics/BigtableTracerCallableTest.java +++ b/google-cloud-bigtable/src/test/java/com/google/cloud/bigtable/data/v2/stub/metrics/BigtableTracerCallableTest.java @@ -32,7 +32,6 @@ import com.google.bigtable.v2.ReadModifyWriteRowResponse; import com.google.bigtable.v2.ReadRowsRequest; import com.google.bigtable.v2.ReadRowsResponse; -import com.google.bigtable.v2.SampleRowKeysRequest; import com.google.bigtable.v2.SampleRowKeysResponse; import com.google.cloud.bigtable.data.v2.BigtableDataSettings; import com.google.cloud.bigtable.data.v2.FakeServiceBuilder; @@ -43,6 +42,8 @@ import com.google.cloud.bigtable.data.v2.models.Query; import com.google.cloud.bigtable.data.v2.models.ReadModifyWriteRow; import com.google.cloud.bigtable.data.v2.models.RowMutation; +import com.google.cloud.bigtable.data.v2.models.SampleRowKeysRequest; +import com.google.cloud.bigtable.data.v2.models.TableId; import com.google.cloud.bigtable.data.v2.stub.EnhancedBigtableStub; import com.google.cloud.bigtable.data.v2.stub.EnhancedBigtableStubSettings; import com.google.common.collect.ImmutableMap; @@ -248,6 +249,24 @@ public void testGFELatencySampleRowKeys() throws InterruptedException { assertThat(latency).isEqualTo(fakeServerTiming.get()); } + @Test + public void testGFELatencySampleRowKeysWithRequest() throws InterruptedException { + stub.sampleRowKeysCallableWithRequest().call(SampleRowKeysRequest.create(TableId.of(TABLE_ID))); + + Thread.sleep(WAIT_FOR_METRICS_TIME_MS); + long latency = + StatsTestUtils.getAggregationValueAsLong( + localStats, + RpcViewConstants.BIGTABLE_GFE_LATENCY_VIEW, + ImmutableMap.of( + RpcMeasureConstants.BIGTABLE_OP, TagValue.create("Bigtable.SampleRowKeys"), + RpcMeasureConstants.BIGTABLE_STATUS, TagValue.create("OK")), + PROJECT_ID, + INSTANCE_ID, + APP_PROFILE_ID); + assertThat(latency).isEqualTo(fakeServerTiming.get()); + } + @Test public void testGFELatencyCheckAndMutateRow() throws InterruptedException { ConditionalRowMutation mutation = @@ -425,7 +444,8 @@ public void mutateRows(MutateRowsRequest request, StreamObserver observer) { + com.google.bigtable.v2.SampleRowKeysRequest request, + StreamObserver observer) { observer.onNext(SampleRowKeysResponse.getDefaultInstance()); observer.onCompleted(); } diff --git a/google-cloud-bigtable/src/test/java/com/google/cloud/bigtable/data/v2/stub/metrics/BuiltinMetricsTracerTest.java b/google-cloud-bigtable/src/test/java/com/google/cloud/bigtable/data/v2/stub/metrics/BuiltinMetricsTracerTest.java index c2be1ea0ff..06b923cad3 100644 --- a/google-cloud-bigtable/src/test/java/com/google/cloud/bigtable/data/v2/stub/metrics/BuiltinMetricsTracerTest.java +++ b/google-cloud-bigtable/src/test/java/com/google/cloud/bigtable/data/v2/stub/metrics/BuiltinMetricsTracerTest.java @@ -17,6 +17,7 @@ import static com.google.api.gax.tracing.ApiTracerFactory.OperationType; import static com.google.common.truth.Truth.assertThat; +import static org.junit.Assert.assertThrows; import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.timeout; import static org.mockito.Mockito.times; @@ -27,6 +28,7 @@ import com.google.api.core.ApiFunction; import com.google.api.core.SettableApiFuture; import com.google.api.gax.batching.Batcher; +import com.google.api.gax.batching.BatchingException; import com.google.api.gax.batching.BatchingSettings; import com.google.api.gax.batching.FlowControlSettings; import com.google.api.gax.grpc.InstantiatingGrpcChannelProvider; @@ -45,6 +47,7 @@ import com.google.bigtable.v2.ResponseParams; import com.google.cloud.bigtable.data.v2.BigtableDataSettings; import com.google.cloud.bigtable.data.v2.FakeServiceBuilder; +import com.google.cloud.bigtable.data.v2.models.AuthorizedViewId; import com.google.cloud.bigtable.data.v2.models.Query; import com.google.cloud.bigtable.data.v2.models.Row; import com.google.cloud.bigtable.data.v2.models.RowMutation; @@ -74,6 +77,7 @@ import io.grpc.StatusRuntimeException; import io.grpc.stub.ServerCallStreamObserver; import io.grpc.stub.StreamObserver; +import java.nio.charset.Charset; import java.util.ArrayList; import java.util.Collections; import java.util.Iterator; @@ -102,7 +106,7 @@ public class BuiltinMetricsTracerTest { private static final String INSTANCE_ID = "fake-instance"; private static final String APP_PROFILE_ID = "default"; private static final String TABLE_ID = "fake-table"; - + private static final String AUTHORIZED_VIEW_ID = "fake-authorized-view"; private static final String BAD_TABLE_ID = "non-exist-table"; private static final String ZONE = "us-west-1"; private static final String CLUSTER = "cluster-0"; @@ -269,6 +273,37 @@ public void testReadRowsOperationLatencies() { assertThat(cluster.getAllValues()).containsExactly(CLUSTER); } + @Test + public void testReadRowsOperationLatenciesOnAuthorizedView() { + when(mockFactory.newTracer(any(), any(), any())) + .thenAnswer( + (Answer) + invocationOnMock -> + new BuiltinMetricsTracer( + OperationType.ServerStreaming, + SpanName.of("Bigtable", "ReadRows"), + statsRecorderWrapper)); + ArgumentCaptor operationLatency = ArgumentCaptor.forClass(Long.class); + + Stopwatch stopwatch = Stopwatch.createStarted(); + Lists.newArrayList( + stub.readRowsCallable() + .call(Query.create(AuthorizedViewId.of(TABLE_ID, AUTHORIZED_VIEW_ID))) + .iterator()); + long elapsed = stopwatch.elapsed(TimeUnit.MILLISECONDS); + + verify(statsRecorderWrapper).putOperationLatencies(operationLatency.capture()); + // verify record operation is only called once + verify(statsRecorderWrapper) + .recordOperation(status.capture(), tableId.capture(), zone.capture(), cluster.capture()); + + assertThat(operationLatency.getValue()).isIn(Range.closed(SERVER_LATENCY, elapsed)); + assertThat(status.getAllValues()).containsExactly("OK"); + assertThat(tableId.getAllValues()).containsExactly(TABLE_ID); + assertThat(zone.getAllValues()).containsExactly(ZONE); + assertThat(cluster.getAllValues()).containsExactly(CLUSTER); + } + @Test public void testGfeMetrics() { when(mockFactory.newTracer(any(), any(), any())) @@ -449,6 +484,55 @@ public void testMutateRowAttemptsTagValues() { assertThat(tableId.getAllValues()).containsExactly(TABLE_ID, TABLE_ID, TABLE_ID); } + @Test + public void testMutateRowsPartialError() throws InterruptedException { + int numMutations = 6; + when(mockFactory.newTracer(any(), any(), any())) + .thenReturn( + new BuiltinMetricsTracer( + OperationType.Unary, SpanName.of("Bigtable", "MutateRows"), statsRecorderWrapper)); + + Batcher batcher = stub.newMutateRowsBatcher(TABLE_ID, null); + for (int i = 0; i < numMutations; i++) { + String key = i % 2 == 0 ? "key" : "fail-key"; + batcher.add(RowMutationEntry.create(key).setCell("f", "q", "v")); + } + + assertThrows(BatchingException.class, () -> batcher.close()); + + int expectedNumRequests = numMutations / batchElementCount; + verify(statsRecorderWrapper, timeout(100).times(expectedNumRequests)) + .recordAttempt(status.capture(), tableId.capture(), zone.capture(), cluster.capture()); + + assertThat(zone.getAllValues()).containsExactly(ZONE, ZONE, ZONE); + assertThat(cluster.getAllValues()).containsExactly(CLUSTER, CLUSTER, CLUSTER); + assertThat(status.getAllValues()).containsExactly("OK", "OK", "OK"); + } + + @Test + public void testMutateRowsRpcError() { + int numMutations = 6; + when(mockFactory.newTracer(any(), any(), any())) + .thenReturn( + new BuiltinMetricsTracer( + OperationType.Unary, SpanName.of("Bigtable", "MutateRows"), statsRecorderWrapper)); + + Batcher batcher = stub.newMutateRowsBatcher(BAD_TABLE_ID, null); + for (int i = 0; i < numMutations; i++) { + batcher.add(RowMutationEntry.create("key").setCell("f", "q", "v")); + } + + assertThrows(BatchingException.class, () -> batcher.close()); + + int expectedNumRequests = numMutations / batchElementCount; + verify(statsRecorderWrapper, timeout(100).times(expectedNumRequests)) + .recordAttempt(status.capture(), tableId.capture(), zone.capture(), cluster.capture()); + + assertThat(zone.getAllValues()).containsExactly("global", "global", "global"); + assertThat(cluster.getAllValues()).containsExactly("unspecified", "unspecified", "unspecified"); + assertThat(status.getAllValues()).containsExactly("NOT_FOUND", "NOT_FOUND", "NOT_FOUND"); + } + @Test public void testReadRowsAttemptsTagValues() { when(mockFactory.newTracer(any(), any(), any())) @@ -644,12 +728,30 @@ public void mutateRow( @Override public void mutateRows( MutateRowsRequest request, StreamObserver responseObserver) { + if (request.getTableName().contains(BAD_TABLE_ID)) { + responseObserver.onError(new StatusRuntimeException(Status.NOT_FOUND)); + return; + } try { Thread.sleep(SERVER_LATENCY); } catch (InterruptedException e) { } MutateRowsResponse.Builder builder = MutateRowsResponse.newBuilder(); for (int i = 0; i < request.getEntriesCount(); i++) { + if (request + .getEntries(i) + .getRowKey() + .toString(Charset.availableCharsets().get("UTF-8")) + .startsWith("fail")) { + builder + .addEntriesBuilder() + .setIndex(i) + .setStatus( + com.google.rpc.Status.newBuilder() + .setCode(com.google.rpc.Code.PERMISSION_DENIED_VALUE) + .build()); + continue; + } builder.addEntriesBuilder().setIndex(i); } responseObserver.onNext(builder.build()); diff --git a/google-cloud-bigtable/src/test/java/com/google/cloud/bigtable/data/v2/stub/metrics/MetricsTracerTest.java b/google-cloud-bigtable/src/test/java/com/google/cloud/bigtable/data/v2/stub/metrics/MetricsTracerTest.java index 2894568f27..15bd9171f0 100644 --- a/google-cloud-bigtable/src/test/java/com/google/cloud/bigtable/data/v2/stub/metrics/MetricsTracerTest.java +++ b/google-cloud-bigtable/src/test/java/com/google/cloud/bigtable/data/v2/stub/metrics/MetricsTracerTest.java @@ -448,7 +448,7 @@ public Object answer(InvocationOnMock invocation) { try (Batcher batcher = new BatcherImpl<>( batchingDescriptor, - stub.bulkMutateRowsCallable().withDefaultCallContext(defaultContext), + stub.internalBulkMutateRowsCallable().withDefaultCallContext(defaultContext), BulkMutation.create(TABLE_ID), settings.getStubSettings().bulkMutateRowsSettings().getBatchingSettings(), Executors.newSingleThreadScheduledExecutor(), diff --git a/google-cloud-bigtable/src/test/java/com/google/cloud/bigtable/data/v2/stub/metrics/StatsHeadersCallableTest.java b/google-cloud-bigtable/src/test/java/com/google/cloud/bigtable/data/v2/stub/metrics/StatsHeadersCallableTest.java index 88a874b8c9..99b0ab5b5e 100644 --- a/google-cloud-bigtable/src/test/java/com/google/cloud/bigtable/data/v2/stub/metrics/StatsHeadersCallableTest.java +++ b/google-cloud-bigtable/src/test/java/com/google/cloud/bigtable/data/v2/stub/metrics/StatsHeadersCallableTest.java @@ -29,7 +29,6 @@ import com.google.bigtable.v2.ReadModifyWriteRowResponse; import com.google.bigtable.v2.ReadRowsRequest; import com.google.bigtable.v2.ReadRowsResponse; -import com.google.bigtable.v2.SampleRowKeysRequest; import com.google.bigtable.v2.SampleRowKeysResponse; import com.google.cloud.bigtable.data.v2.BigtableDataSettings; import com.google.cloud.bigtable.data.v2.FakeServiceBuilder; @@ -40,6 +39,8 @@ import com.google.cloud.bigtable.data.v2.models.ReadModifyWriteRow; import com.google.cloud.bigtable.data.v2.models.RowMutation; import com.google.cloud.bigtable.data.v2.models.RowMutationEntry; +import com.google.cloud.bigtable.data.v2.models.SampleRowKeysRequest; +import com.google.cloud.bigtable.data.v2.models.TableId; import com.google.cloud.bigtable.data.v2.stub.EnhancedBigtableStub; import com.google.cloud.bigtable.data.v2.stub.EnhancedBigtableStubSettings; import com.google.common.collect.Queues; @@ -139,6 +140,15 @@ public void testSampleRowKeysHeaders() throws Exception { verifyHeaders(attemptCounts, startTimestamp); } + @Test + public void testSampleRowKeysWithRequestHeaders() throws Exception { + long startTimestamp = System.currentTimeMillis() * 1000; + stub.sampleRowKeysCallableWithRequest() + .call(SampleRowKeysRequest.create(TableId.of(TABLE_ID))) + .get(0); + verifyHeaders(attemptCounts, startTimestamp); + } + @Test public void testCheckAndMutateHeaders() throws Exception { long startTimestamp = System.currentTimeMillis() * 1000; @@ -233,7 +243,8 @@ public void mutateRows(MutateRowsRequest request, StreamObserver observer) { + com.google.bigtable.v2.SampleRowKeysRequest request, + StreamObserver observer) { if (callCount.get() < attemptCounts - 1) { callCount.incrementAndGet(); observer.onError(new StatusRuntimeException(Status.UNAVAILABLE)); diff --git a/google-cloud-bigtable/src/test/java/com/google/cloud/bigtable/data/v2/stub/mutaterows/MutateRowsAttemptCallableTest.java b/google-cloud-bigtable/src/test/java/com/google/cloud/bigtable/data/v2/stub/mutaterows/MutateRowsAttemptCallableTest.java index e5d12ccaeb..6dd1ff9bd0 100644 --- a/google-cloud-bigtable/src/test/java/com/google/cloud/bigtable/data/v2/stub/mutaterows/MutateRowsAttemptCallableTest.java +++ b/google-cloud-bigtable/src/test/java/com/google/cloud/bigtable/data/v2/stub/mutaterows/MutateRowsAttemptCallableTest.java @@ -44,8 +44,6 @@ import java.util.List; import java.util.Set; import java.util.concurrent.Callable; -import java.util.concurrent.ExecutionException; -import org.junit.Assert; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; @@ -101,14 +99,18 @@ public void singleEntrySuccessTest() throws Exception { attemptCallable.setExternalFuture(parentFuture); attemptCallable.call(); - // Attempt completed successfully and the useless response has been suppressed - assertThat(parentFuture.attemptFuture.get()).isNull(); + // Attempt completed successfully + MutateRowsAttemptResult result = parentFuture.attemptFuture.get(); + + assertThat(result).isNotNull(); + assertThat(result.getFailedMutations()).hasSize(0); + assertThat(result.getIsRetryable()).isFalse(); // innerCallable received the request assertThat(innerCallable.lastRequest).isEqualTo(request); } @Test - public void missingEntry() { + public void missingEntry() throws Exception { MutateRowsRequest request = MutateRowsRequest.newBuilder() .addEntries(Entry.getDefaultInstance()) @@ -125,14 +127,10 @@ public void missingEntry() { attemptCallable.setExternalFuture(parentFuture); attemptCallable.call(); - ExecutionException executionException = - Assert.assertThrows(ExecutionException.class, () -> parentFuture.attemptFuture.get()); - assertThat(executionException).hasCauseThat().isInstanceOf(MutateRowsException.class); - MutateRowsException e = (MutateRowsException) executionException.getCause(); + MutateRowsAttemptResult result = parentFuture.attemptFuture.get(); - assertThat(e).hasMessageThat().contains("Some mutations failed to apply"); - assertThat(e.getFailedMutations()).hasSize(1); - FailedMutation failedMutation = e.getFailedMutations().get(0); + assertThat(result.getFailedMutations()).hasSize(1); + FailedMutation failedMutation = result.getFailedMutations().get(0); assertThat(failedMutation.getIndex()).isEqualTo(1); assertThat(failedMutation.getError()) .hasMessageThat() @@ -163,7 +161,7 @@ public void testNoRpcTimeout() { } @Test - public void mixedTest() { + public void mixedTest() throws Exception { // Setup the request & response MutateRowsRequest request = MutateRowsRequest.newBuilder() @@ -194,20 +192,11 @@ public void mixedTest() { // Make the only call attemptCallable.call(); - // Overall error expectations - Throwable actualError = null; - try { - parentFuture.attemptFuture.get(); - } catch (Throwable t) { - actualError = t.getCause(); - } - - assertThat(actualError).isInstanceOf(MutateRowsException.class); - assertThat(((MutateRowsException) actualError).isRetryable()).isTrue(); + MutateRowsAttemptResult result = parentFuture.attemptFuture.get(); // Entry expectations @SuppressWarnings("ConstantConditions") - List failedMutations = ((MutateRowsException) actualError).getFailedMutations(); + List failedMutations = result.getFailedMutations(); assertThat(failedMutations).hasSize(2); assertThat(failedMutations.get(0).getIndex()).isEqualTo(1); @@ -222,7 +211,7 @@ public void mixedTest() { } @Test - public void nextAttemptTest() { + public void nextAttemptTest() throws Exception { // Setup the request & response for the first call MutateRowsRequest request = MutateRowsRequest.newBuilder() @@ -267,19 +256,11 @@ public void nextAttemptTest() { assertThat(innerCallable.lastRequest.getEntries(0).getRowKey()) .isEqualTo(ByteString.copyFromUtf8("1-unavailable")); - // Overall error expectations - Throwable actualError = null; - try { - parentFuture.attemptFuture.get(); - } catch (Throwable t) { - actualError = t.getCause(); - } - assertThat(actualError).isInstanceOf(MutateRowsException.class); - assertThat(((MutateRowsException) actualError).isRetryable()).isFalse(); + MutateRowsAttemptResult result = parentFuture.attemptFuture.get(); // Entry expectations @SuppressWarnings("ConstantConditions") - List failedMutations = ((MutateRowsException) actualError).getFailedMutations(); + List failedMutations = result.getFailedMutations(); assertThat(failedMutations).hasSize(1); assertThat(failedMutations.get(0).getIndex()).isEqualTo(2); @@ -411,8 +392,9 @@ public ApiFuture> futureCall( } } - static class MockRetryingFuture extends AbstractApiFuture implements RetryingFuture { - ApiFuture attemptFuture; + static class MockRetryingFuture extends AbstractApiFuture + implements RetryingFuture { + ApiFuture attemptFuture; TimedAttemptSettings timedAttemptSettings; @@ -433,7 +415,7 @@ static class MockRetryingFuture extends AbstractApiFuture implements Retry } @Override - public void setAttemptFuture(ApiFuture attemptFuture) { + public void setAttemptFuture(ApiFuture attemptFuture) { this.attemptFuture = attemptFuture; } @@ -443,17 +425,17 @@ public TimedAttemptSettings getAttemptSettings() { } @Override - public Callable getCallable() { + public Callable getCallable() { throw new UnsupportedOperationException("not used"); } @Override - public ApiFuture peekAttemptResult() { + public ApiFuture peekAttemptResult() { throw new UnsupportedOperationException("not used"); } @Override - public ApiFuture getAttemptResult() { + public ApiFuture getAttemptResult() { throw new UnsupportedOperationException("not used"); } } diff --git a/google-cloud-bigtable/src/test/java/com/google/cloud/bigtable/data/v2/stub/mutaterows/MutateRowsBatchingDescriptorTest.java b/google-cloud-bigtable/src/test/java/com/google/cloud/bigtable/data/v2/stub/mutaterows/MutateRowsBatchingDescriptorTest.java index 237444ba84..c5f11d91d5 100644 --- a/google-cloud-bigtable/src/test/java/com/google/cloud/bigtable/data/v2/stub/mutaterows/MutateRowsBatchingDescriptorTest.java +++ b/google-cloud-bigtable/src/test/java/com/google/cloud/bigtable/data/v2/stub/mutaterows/MutateRowsBatchingDescriptorTest.java @@ -16,21 +16,26 @@ package com.google.cloud.bigtable.data.v2.stub.mutaterows; import static com.google.common.truth.Truth.assertThat; +import static org.junit.Assert.assertThrows; import com.google.api.core.SettableApiFuture; import com.google.api.gax.batching.BatchEntry; import com.google.api.gax.batching.BatchResource; import com.google.api.gax.batching.BatchingRequestBuilder; import com.google.api.gax.grpc.GrpcStatusCode; +import com.google.api.gax.rpc.ApiExceptionFactory; import com.google.api.gax.rpc.DeadlineExceededException; +import com.google.api.gax.rpc.InternalException; import com.google.api.gax.rpc.UnavailableException; import com.google.cloud.bigtable.data.v2.internal.RequestContext; import com.google.cloud.bigtable.data.v2.models.BulkMutation; import com.google.cloud.bigtable.data.v2.models.MutateRowsException; +import com.google.cloud.bigtable.data.v2.models.MutateRowsException.FailedMutation; import com.google.cloud.bigtable.data.v2.models.Mutation; import com.google.cloud.bigtable.data.v2.models.RowMutationEntry; import com.google.common.collect.ImmutableList; import io.grpc.Status; +import java.util.Arrays; import java.util.List; import java.util.concurrent.ExecutionException; import org.junit.Test; @@ -92,11 +97,58 @@ public void splitResponseTest() { assertThat(batchResponse.get(1).getResultFuture().isDone()).isFalse(); MutateRowsBatchingDescriptor underTest = new MutateRowsBatchingDescriptor(); - underTest.splitResponse(null, batchResponse); + underTest.splitResponse(MutateRowsAttemptResult.success(), batchResponse); assertThat(batchResponse.get(0).getResultFuture().isDone()).isTrue(); assertThat(batchResponse.get(1).getResultFuture().isDone()).isTrue(); } + @Test + public void splitResponsePartialErrorsTest() { + BatchEntry batchEntry1 = + BatchEntry.create( + RowMutationEntry.create("key1").deleteRow(), SettableApiFuture.create()); + BatchEntry batchEntry2 = + BatchEntry.create( + RowMutationEntry.create("key2").deleteRow(), SettableApiFuture.create()); + + List> batchResponse = + ImmutableList.of(batchEntry1, batchEntry2); + assertThat(batchResponse.get(0).getResultFuture().isDone()).isFalse(); + assertThat(batchResponse.get(1).getResultFuture().isDone()).isFalse(); + + MutateRowsBatchingDescriptor underTest = new MutateRowsBatchingDescriptor(); + underTest.splitResponse( + MutateRowsAttemptResult.create( + Arrays.asList( + FailedMutation.create( + 0, + ApiExceptionFactory.createException( + "error message", + null, + GrpcStatusCode.of(io.grpc.Status.Code.INTERNAL), + false))), + true), + batchResponse); + assertThat(batchResponse.get(0).getResultFuture().isDone()).isTrue(); + assertThat(batchResponse.get(1).getResultFuture().isDone()).isTrue(); + + Throwable unexpectedError = null; + try { + batchResponse.get(1).getResultFuture().get(); + + } catch (Throwable t) { + unexpectedError = t; + } + assertThat(unexpectedError).isNull(); + + Throwable actualError = + assertThrows(ExecutionException.class, () -> batchResponse.get(0).getResultFuture().get()) + .getCause(); + + assertThat(actualError).isInstanceOf(InternalException.class); + assertThat(actualError).hasMessageThat().contains("error message"); + } + @Test public void splitExceptionTest() { BatchEntry batchEntry1 = @@ -140,6 +192,7 @@ public void splitExceptionWithFailedMutationsTest() { MutateRowsException serverError = MutateRowsException.create( null, + GrpcStatusCode.of(Status.Code.UNAVAILABLE), ImmutableList.of( MutateRowsException.FailedMutation.create( 0, diff --git a/google-cloud-bigtable/src/test/java/com/google/cloud/bigtable/data/v2/stub/mutaterows/MutateRowsErrorConverterUnaryCallableTest.java b/google-cloud-bigtable/src/test/java/com/google/cloud/bigtable/data/v2/stub/mutaterows/MutateRowsErrorConverterUnaryCallableTest.java new file mode 100644 index 0000000000..170aa66188 --- /dev/null +++ b/google-cloud-bigtable/src/test/java/com/google/cloud/bigtable/data/v2/stub/mutaterows/MutateRowsErrorConverterUnaryCallableTest.java @@ -0,0 +1,108 @@ +/* + * Copyright 2024 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.stub.mutaterows; + +import static com.google.common.truth.Truth.assertThat; + +import com.google.api.core.SettableApiFuture; +import com.google.api.gax.grpc.GrpcStatusCode; +import com.google.api.gax.rpc.ApiExceptionFactory; +import com.google.api.gax.rpc.UnaryCallable; +import com.google.cloud.bigtable.data.v2.models.BulkMutation; +import com.google.cloud.bigtable.data.v2.models.MutateRowsException; +import com.google.cloud.bigtable.data.v2.models.MutateRowsException.FailedMutation; +import com.google.cloud.bigtable.data.v2.stub.MutateRowsErrorConverterUnaryCallable; +import java.util.Arrays; +import org.junit.Assert; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; +import org.mockito.ArgumentCaptor; +import org.mockito.Captor; +import org.mockito.Mock; +import org.mockito.Mockito; +import org.mockito.junit.MockitoJUnit; +import org.mockito.junit.MockitoRule; + +@RunWith(JUnit4.class) +public class MutateRowsErrorConverterUnaryCallableTest { + @Mock private UnaryCallable innerCallable; + @Captor private ArgumentCaptor innerMutation; + private SettableApiFuture innerResult; + + @Rule public final MockitoRule mockitoRule = MockitoJUnit.rule(); + + @Before + public void setUp() { + innerResult = SettableApiFuture.create(); + Mockito.when(innerCallable.futureCall(innerMutation.capture(), Mockito.any())) + .thenReturn(innerResult); + } + + @Test + public void testSuccess() { + MutateRowsErrorConverterUnaryCallable callable = + new MutateRowsErrorConverterUnaryCallable(innerCallable); + + innerResult.set(MutateRowsAttemptResult.success()); + + Throwable unexpectedError = null; + try { + callable.call(BulkMutation.create("fake-table")); + } catch (Throwable t) { + unexpectedError = t; + } + assertThat(unexpectedError).isNull(); + } + + @Test + public void testPartialFailure() { + MutateRowsErrorConverterUnaryCallable callable = + new MutateRowsErrorConverterUnaryCallable(innerCallable); + + innerResult.set( + MutateRowsAttemptResult.create( + Arrays.asList( + FailedMutation.create( + 0, + ApiExceptionFactory.createException( + null, GrpcStatusCode.of(io.grpc.Status.Code.INTERNAL), false))), + true)); + + MutateRowsException exception = + Assert.assertThrows( + MutateRowsException.class, () -> callable.call(BulkMutation.create("fake-table"))); + + assertThat(exception).isInstanceOf(MutateRowsException.class); + assertThat((exception).isRetryable()).isTrue(); + } + + @Test + public void testRPCFailure() { + MutateRowsErrorConverterUnaryCallable callable = + new MutateRowsErrorConverterUnaryCallable(innerCallable); + + innerResult.setException(new Exception("RPC error")); + + Exception exception = + Assert.assertThrows( + Exception.class, () -> callable.call(BulkMutation.create("fake-table"))); + + assertThat(exception).isInstanceOf(Exception.class); + } +} diff --git a/google-cloud-bigtable/src/test/java/com/google/cloud/bigtable/misc_utilities/AuthorizedViewTestHelper.java b/google-cloud-bigtable/src/test/java/com/google/cloud/bigtable/misc_utilities/AuthorizedViewTestHelper.java new file mode 100644 index 0000000000..83c40403f8 --- /dev/null +++ b/google-cloud-bigtable/src/test/java/com/google/cloud/bigtable/misc_utilities/AuthorizedViewTestHelper.java @@ -0,0 +1,45 @@ +/* + * Copyright 2024 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.misc_utilities; + +import com.google.cloud.bigtable.admin.v2.models.AuthorizedView; +import com.google.cloud.bigtable.admin.v2.models.CreateAuthorizedViewRequest; +import com.google.cloud.bigtable.admin.v2.models.FamilySubsets; +import com.google.cloud.bigtable.admin.v2.models.SubsetView; +import com.google.cloud.bigtable.test_helpers.env.TestEnvRule; +import java.util.UUID; + +public class AuthorizedViewTestHelper { + public static String AUTHORIZED_VIEW_ROW_PREFIX = "row#"; + public static String AUTHORIZED_VIEW_COLUMN_QUALIFIER = "qualifier"; + + public static AuthorizedView createTestAuthorizedView(TestEnvRule testEnvRule) { + String tableId = testEnvRule.env().getTableId(); + String authorizedViewId = UUID.randomUUID().toString(); + CreateAuthorizedViewRequest request = + CreateAuthorizedViewRequest.of(tableId, authorizedViewId) + .setAuthorizedViewType( + SubsetView.create() + .addRowPrefix(AUTHORIZED_VIEW_ROW_PREFIX) + .setFamilySubsets( + testEnvRule.env().getFamilyId(), + FamilySubsets.create() + .addQualifierPrefix(AUTHORIZED_VIEW_COLUMN_QUALIFIER))) + .setDeletionProtection(false); + return testEnvRule.env().getTableAdminClient().createAuthorizedView(request); + } +} diff --git a/grpc-google-cloud-bigtable-admin-v2/pom.xml b/grpc-google-cloud-bigtable-admin-v2/pom.xml index 583a5ed3e9..4606ec8b84 100644 --- a/grpc-google-cloud-bigtable-admin-v2/pom.xml +++ b/grpc-google-cloud-bigtable-admin-v2/pom.xml @@ -4,13 +4,13 @@ 4.0.0 com.google.api.grpc grpc-google-cloud-bigtable-admin-v2 - 2.36.0 + 2.37.0 grpc-google-cloud-bigtable-admin-v2 GRPC library for grpc-google-cloud-bigtable-admin-v2 com.google.cloud google-cloud-bigtable-parent - 2.36.0 + 2.37.0 @@ -18,14 +18,14 @@ com.google.cloud google-cloud-bigtable-deps-bom - 2.36.0 + 2.37.0 pom import com.google.cloud google-cloud-bigtable-bom - 2.36.0 + 2.37.0 pom import diff --git a/grpc-google-cloud-bigtable-v2/pom.xml b/grpc-google-cloud-bigtable-v2/pom.xml index 46c36f2bf3..3e89608789 100644 --- a/grpc-google-cloud-bigtable-v2/pom.xml +++ b/grpc-google-cloud-bigtable-v2/pom.xml @@ -4,13 +4,13 @@ 4.0.0 com.google.api.grpc grpc-google-cloud-bigtable-v2 - 2.36.0 + 2.37.0 grpc-google-cloud-bigtable-v2 GRPC library for grpc-google-cloud-bigtable-v2 com.google.cloud google-cloud-bigtable-parent - 2.36.0 + 2.37.0 @@ -18,14 +18,14 @@ com.google.cloud google-cloud-bigtable-deps-bom - 2.36.0 + 2.37.0 pom import com.google.cloud google-cloud-bigtable-bom - 2.36.0 + 2.37.0 pom import diff --git a/owlbot.py b/owlbot.py index f0c954ff62..c9aac4e206 100644 --- a/owlbot.py +++ b/owlbot.py @@ -105,6 +105,7 @@ def make_internal_only(sources): # todo remove once template is updated '.github/ISSUE_TEMPLATE/bug_report.md', '.github/PULL_REQUEST_TEMPLATE.md', + '.github/trusted-contribution.yml', 'CONTRIBUTING.md', # exclude autogen 'codecov.yaml', diff --git a/pom.xml b/pom.xml index f939d702ad..ffa93e753e 100644 --- a/pom.xml +++ b/pom.xml @@ -4,7 +4,7 @@ google-cloud-bigtable-parent pom - 2.36.0 + 2.37.0 Google Cloud Bigtable Parent https://github.com/googleapis/java-bigtable @@ -14,7 +14,7 @@ com.google.cloud sdk-platform-java-config - 3.27.0 + 3.28.1 @@ -153,27 +153,27 @@ com.google.api.grpc proto-google-cloud-bigtable-v2 - 2.36.0 + 2.37.0 com.google.api.grpc proto-google-cloud-bigtable-admin-v2 - 2.36.0 + 2.37.0 com.google.api.grpc grpc-google-cloud-bigtable-v2 - 2.36.0 + 2.37.0 com.google.api.grpc grpc-google-cloud-bigtable-admin-v2 - 2.36.0 + 2.37.0 com.google.cloud google-cloud-bigtable - 2.36.0 + 2.37.0 diff --git a/proto-google-cloud-bigtable-admin-v2/pom.xml b/proto-google-cloud-bigtable-admin-v2/pom.xml index 8149c4b5dd..c06610f581 100644 --- a/proto-google-cloud-bigtable-admin-v2/pom.xml +++ b/proto-google-cloud-bigtable-admin-v2/pom.xml @@ -4,13 +4,13 @@ 4.0.0 com.google.api.grpc proto-google-cloud-bigtable-admin-v2 - 2.36.0 + 2.37.0 proto-google-cloud-bigtable-admin-v2 PROTO library for proto-google-cloud-bigtable-admin-v2 com.google.cloud google-cloud-bigtable-parent - 2.36.0 + 2.37.0 @@ -18,14 +18,14 @@ com.google.cloud google-cloud-bigtable-deps-bom - 2.36.0 + 2.37.0 pom import com.google.cloud google-cloud-bigtable-bom - 2.36.0 + 2.37.0 pom import diff --git a/proto-google-cloud-bigtable-v2/pom.xml b/proto-google-cloud-bigtable-v2/pom.xml index 394e2f26aa..0a30e75756 100644 --- a/proto-google-cloud-bigtable-v2/pom.xml +++ b/proto-google-cloud-bigtable-v2/pom.xml @@ -4,13 +4,13 @@ 4.0.0 com.google.api.grpc proto-google-cloud-bigtable-v2 - 2.36.0 + 2.37.0 proto-google-cloud-bigtable-v2 PROTO library for proto-google-cloud-bigtable-v2 com.google.cloud google-cloud-bigtable-parent - 2.36.0 + 2.37.0 @@ -18,14 +18,14 @@ com.google.cloud google-cloud-bigtable-deps-bom - 2.36.0 + 2.37.0 pom import com.google.cloud google-cloud-bigtable-bom - 2.36.0 + 2.37.0 pom import diff --git a/samples/install-without-bom/pom.xml b/samples/install-without-bom/pom.xml index 60b0a18ca5..48f9dd3756 100644 --- a/samples/install-without-bom/pom.xml +++ b/samples/install-without-bom/pom.xml @@ -29,7 +29,7 @@ com.google.cloud google-cloud-bigtable - 2.35.1 + 2.36.0 diff --git a/samples/snapshot/pom.xml b/samples/snapshot/pom.xml index 6baa349a20..60c3dd1b29 100644 --- a/samples/snapshot/pom.xml +++ b/samples/snapshot/pom.xml @@ -28,7 +28,7 @@ com.google.cloud google-cloud-bigtable - 2.36.0 + 2.37.0 diff --git a/samples/snippets/src/test/java/com/example/bigtable/QuickstartTest.java b/samples/snippets/src/test/java/com/example/bigtable/QuickstartTest.java index 2471f26b16..ba00ba872a 100644 --- a/samples/snippets/src/test/java/com/example/bigtable/QuickstartTest.java +++ b/samples/snippets/src/test/java/com/example/bigtable/QuickstartTest.java @@ -18,6 +18,12 @@ import static org.junit.Assert.assertThat; +import com.google.cloud.bigtable.admin.v2.BigtableTableAdminClient; +import com.google.cloud.bigtable.admin.v2.models.CreateTableRequest; +import com.google.cloud.bigtable.data.v2.BigtableDataClient; +import com.google.cloud.bigtable.data.v2.models.Row; +import com.google.cloud.bigtable.data.v2.models.RowMutation; +import java.io.IOException; import org.hamcrest.CoreMatchers; import org.junit.BeforeClass; import org.junit.Test; @@ -28,8 +34,25 @@ public class QuickstartTest extends BigtableBaseTest { private static final String TABLE_ID = "quickstart-table"; @BeforeClass - public static void beforeClass() { + public static void beforeClass() throws IOException { initializeVariables(); + + // set up required table and row data if not present + try (BigtableTableAdminClient tableAdminClient = + BigtableTableAdminClient.create(projectId, instanceId)) { + String columnFamily = "cf1"; + if (!tableAdminClient.exists(TABLE_ID)) { + tableAdminClient.createTable(CreateTableRequest.of(TABLE_ID).addFamily(columnFamily)); + } + try (BigtableDataClient dataClient = BigtableDataClient.create(projectId, instanceId)) { + String rowKey = "r1"; + Row row = dataClient.readRow(TABLE_ID, rowKey); + if (row == null) { + dataClient.mutateRow( + RowMutation.create(TABLE_ID, rowKey).setCell(columnFamily, "c1", "quickstart")); + } + } + } } @Test diff --git a/test-proxy/pom.xml b/test-proxy/pom.xml index 3b0004ffd0..06c6c0e01d 100644 --- a/test-proxy/pom.xml +++ b/test-proxy/pom.xml @@ -12,11 +12,11 @@ google-cloud-bigtable-parent com.google.cloud - 2.36.0 + 2.37.0 - 2.36.0 + 2.37.0 diff --git a/versions.txt b/versions.txt index 8bec74f395..52c4512fef 100644 --- a/versions.txt +++ b/versions.txt @@ -1,10 +1,10 @@ # Format: # module:released-version:current-version -google-cloud-bigtable:2.36.0:2.36.0 -grpc-google-cloud-bigtable-admin-v2:2.36.0:2.36.0 -grpc-google-cloud-bigtable-v2:2.36.0:2.36.0 -proto-google-cloud-bigtable-admin-v2:2.36.0:2.36.0 -proto-google-cloud-bigtable-v2:2.36.0:2.36.0 -google-cloud-bigtable-emulator:0.173.0:0.173.0 -google-cloud-bigtable-emulator-core:0.173.0:0.173.0 +google-cloud-bigtable:2.37.0:2.37.0 +grpc-google-cloud-bigtable-admin-v2:2.37.0:2.37.0 +grpc-google-cloud-bigtable-v2:2.37.0:2.37.0 +proto-google-cloud-bigtable-admin-v2:2.37.0:2.37.0 +proto-google-cloud-bigtable-v2:2.37.0:2.37.0 +google-cloud-bigtable-emulator:0.174.0:0.174.0 +google-cloud-bigtable-emulator-core:0.174.0:0.174.0