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 889598020a73..136fc8f3abbf 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 @@ -24,13 +24,16 @@ 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.DeleteSchemaBundleRequest; 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.GetSchemaBundleRequest; 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.ListSchemaBundlesRequest; import com.google.bigtable.admin.v2.ListTablesRequest; import com.google.bigtable.admin.v2.RestoreTableMetadata; import com.google.bigtable.admin.v2.Table.ClusterState; @@ -41,6 +44,8 @@ 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.ListSchemaBundlesPage; +import com.google.cloud.bigtable.admin.v2.BaseBigtableTableAdminClient.ListSchemaBundlesPagedResponse; 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; @@ -50,6 +55,7 @@ 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.CreateSchemaBundleRequest; 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.GCRules; @@ -57,9 +63,11 @@ import com.google.cloud.bigtable.admin.v2.models.OptimizeRestoredTableOperationToken; 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.SchemaBundle; 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.UpdateSchemaBundleRequest; import com.google.cloud.bigtable.admin.v2.models.UpdateTableRequest; import com.google.cloud.bigtable.admin.v2.stub.EnhancedBigtableTableAdminStub; import com.google.cloud.bigtable.data.v2.internal.TableAdminRequestContext; @@ -1797,6 +1805,340 @@ public ApiFuture deleteAuthorizedViewAsync(String tableId, String authoriz return transformToVoid(this.stub.deleteAuthorizedViewCallable().futureCall(request)); } + /** + * Creates a new schema bundle with the specified configuration. + * + *

Sample code: + * + *

{@code
+   * CreateSchemaBundleRequest request = CreateSchemaBundleRequest.of("my-table", "my-new-schema-bundle")
+   *     .setDeletionProtection(true)
+   *     .setSchemaBundleType(
+   *         SubsetView.create()
+   *             .addRowPrefix("row#")
+   *             .addFamilySubsets(
+   *                 "my-family", FamilySubsets.create().addQualifier("column")));
+   *
+   * SchemaBundle response = client.createSchemaBundle(request);
+   * }
+ * + * @see CreateSchemaBundleRequest for available options. + */ + public SchemaBundle createSchemaBundle(CreateSchemaBundleRequest request) { + return ApiExceptions.callAndTranslateApiException(createSchemaBundleAsync(request)); + } + + /** + * Asynchronously creates a new schema bundle with the specified configuration. + * + *

Sample code: + * + *

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

Sample code: + * + *

{@code
+   * SchemaBundle existingSchemaBundle = client.getSchemaBundle("my-table", "my-schema-bundle");
+   *
+   * UpdateSchemaBundleRequest request = UpdateSchemaBundleRequest.of(existingSchemaBundle).setDeletionProtection(true);
+   *
+   * SchemaBundle response = client.updateSchemaBundle(request);
+   * }
+ * + * @see UpdateSchemaBundleRequest for available options. + */ + public SchemaBundle updateSchemaBundle(UpdateSchemaBundleRequest request) { + return ApiExceptions.callAndTranslateApiException(updateSchemaBundleAsync(request)); + } + + /** + * Asynchronously updates an existing schema bundle with the specified configuration. + * + *

Sample code: + * + *

{@code
+   * SchemaBundle existingSchemaBundle = client.getSchemaBundle("my-table", "my-schema-bundle");
+   *
+   * UpdateSchemaBundleRequest request = UpdateSchemaBundleRequest.of(existingSchemaBundle).setDeletionProtection(true);
+   *
+   * ApiFuture future = client.updateSchemaBundleAsync(request);
+   *
+   * ApiFutures.addCallback(
+   *     future,
+   *     new ApiFutureCallback() {
+   *       public void onSuccess(SchemaBundle schemaBundle) {
+   *         System.out.println("Successfully updated the schema bundle: " + schemaBundle.getId());
+   *       }
+   *
+   *       public void onFailure(Throwable t) {
+   *         t.printStackTrace();
+   *       }
+   *     },
+   *     MoreExecutors.directExecutor());
+   * }
+ * + * @see UpdateSchemaBundleRequest for available options. + */ + public ApiFuture updateSchemaBundleAsync(UpdateSchemaBundleRequest request) { + return ApiFutures.transform( + stub.updateSchemaBundleOperationCallable() + .futureCall(request.toProto(projectId, instanceId)), + new ApiFunction() { + @Override + public SchemaBundle apply(com.google.bigtable.admin.v2.SchemaBundle schemaBundleProto) { + return SchemaBundle.fromProto(schemaBundleProto); + } + }, + MoreExecutors.directExecutor()); + } + + /** + * Gets an schema bundle with the specified schema bundle ID in the specified table. + * + *

Sample code: + * + *

{@code
+   * SchemaBundle schemaBundle = client.getSchemaBundle("my-table", "my-schema-bundle");
+   * }
+ */ + public SchemaBundle getSchemaBundle(String tableId, String schemaBundleId) { + return ApiExceptions.callAndTranslateApiException( + getSchemaBundleAsync(tableId, schemaBundleId)); + } + + /** + * Asynchronously gets an schema bundle with the specified schema bundle ID in the specified + * table. + * + *

Sample code: + * + *

{@code
+   * ApiFuture future = client.getSchemaBundleAsync("my-table", "my-schema-bundle");
+   *
+   * ApiFutures.addCallback(
+   *     future,
+   *     new ApiFutureCallback() {
+   *       public void onSuccess(SchemaBundle schemaBundle) {
+   *         System.out.println("Successfully get the schema bundle: " + schemaBundle.getId());
+   *       }
+   *
+   *       public void onFailure(Throwable t) {
+   *         t.printStackTrace();
+   *       }
+   *     },
+   *     MoreExecutors.directExecutor());
+   * }
+ */ + public ApiFuture getSchemaBundleAsync(String tableId, String schemaBundleId) { + GetSchemaBundleRequest request = + GetSchemaBundleRequest.newBuilder() + .setName( + NameUtil.formatSchemaBundleName(projectId, instanceId, tableId, schemaBundleId)) + .build(); + return ApiFutures.transform( + stub.getSchemaBundleCallable().futureCall(request), + new ApiFunction() { + @Override + public SchemaBundle apply(com.google.bigtable.admin.v2.SchemaBundle schemaBundleProto) { + return SchemaBundle.fromProto(schemaBundleProto); + } + }, + MoreExecutors.directExecutor()); + } + + /** + * Lists all schema bundle IDs in the specified table. + * + *

Sample code: + * + *

{@code
+   * List schemaBundles = client.listSchemaBundles("my-table");
+   * }
+ */ + public List listSchemaBundles(String tableId) { + return ApiExceptions.callAndTranslateApiException(listSchemaBundlesAsync(tableId)); + } + + /** + * Asynchronously lists all schema bundle IDs in the specified table. + * + *

Sample code: + * + *

{@code
+   * ApiFuture> future = client.listSchemaBundlesAsync("my-table");
+   *
+   * ApiFutures.addCallback(
+   *     future,
+   *     new ApiFutureCallback>() {
+   *       public void onSuccess(List schemaBundleIds) {
+   *         System.out.println("Successfully get list of schema bundles:");
+   *         for (SchemaBundle schemaBundleId : schemaBundleIds) {
+   *           System.out.println(schemaBundleId);
+   *         }
+   *       }
+   *
+   *       public void onFailure(Throwable t) {
+   *         t.printStackTrace();
+   *       }
+   *     },
+   *     MoreExecutors.directExecutor());
+   * }
+ */ + public ApiFuture> listSchemaBundlesAsync(String tableId) { + ListSchemaBundlesRequest request = + ListSchemaBundlesRequest.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.listSchemaBundlesPagedCallable().futureCall(request), + new ApiFunction() { + @Override + public ListSchemaBundlesPage apply(ListSchemaBundlesPagedResponse response) { + return response.getPage(); + } + }, + MoreExecutors.directExecutor()); + + // Fetches the rest of the pages by chaining the futures. + ApiFuture> allProtos = + ApiFutures.transformAsync( + firstPageFuture, + new ApiAsyncFunction< + ListSchemaBundlesPage, List>() { + List responseAccumulator = + Lists.newArrayList(); + + @Override + public ApiFuture> apply( + ListSchemaBundlesPage 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.SchemaBundle proto : protos) { + results.add(NameUtil.extractSchemaBundleIdFromSchemaBundleName(proto.getName())); + } + return results; + } + }, + MoreExecutors.directExecutor()); + } + + /** + * Deletes an schema bundle with the specified schema bundle ID in the specified table. Note that + * the deletion is prohibited if the schema bundle has deletion_protection field set to true. + * + *

Sample code: + * + *

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

Sample code: + * + *

{@code
+   * ApiFuture future = client.deleteSchemaBundleAsync("my-table", "my-schema-bundle");
+   *
+   * ApiFutures.addCallback(
+   *   future,
+   *   new ApiFutureCallback() {
+   *     public void onSuccess(Void ignored) {
+   *       System.out.println("Successfully deleted the schema bundle");
+   *     }
+   *
+   *     public void onFailure(Throwable t) {
+   *       t.printStackTrace();
+   *     }
+   *   },
+   *   MoreExecutors.directExecutor()
+   * );
+   * }
+ */ + public ApiFuture deleteSchemaBundleAsync(String tableId, String schemaBundleId) { + DeleteSchemaBundleRequest request = + DeleteSchemaBundleRequest.newBuilder() + .setName( + NameUtil.formatSchemaBundleName(projectId, instanceId, tableId, schemaBundleId)) + .build(); + + return transformToVoid(this.stub.deleteSchemaBundleCallable().futureCall(request)); + } + /** * Helper method to construct the table name in format: * projects/{project}/instances/{instance}/tables/{tableId} 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 ec2c3a0b585b..3918b40dd00d 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 @@ -38,6 +38,9 @@ public class NameUtil { private static final Pattern AUTHORIZED_VIEW_PATTERN = Pattern.compile("projects/([^/]+)/instances/([^/]+)/tables/([^/]+)/authorizedViews/([^/]+)"); + private static final Pattern SCHEMA_BUNDLE_PATTERN = + Pattern.compile("projects/([^/]+)/instances/([^/]+)/tables/([^/]+)/schemaBundles/([^/]+)"); + public static String formatProjectName(String projectId) { return "projects/" + projectId; } @@ -74,6 +77,11 @@ public static String formatAuthorizedViewName( return formatTableName(projectId, instanceId, tableId) + "/authorizedViews/" + viewId; } + public static String formatSchemaBundleName( + String projectId, String instanceId, String tableId, String bundleId) { + return formatTableName(projectId, instanceId, tableId) + "/schemaBundles/" + bundleId; + } + public static String extractTableIdFromTableName(String fullTableName) { Matcher matcher = TABLE_PATTERN.matcher(fullTableName); if (!matcher.matches()) { @@ -99,6 +107,14 @@ public static String extractAuthorizedViewIdFromAuthorizedViewName( return matcher.group(4); } + public static String extractSchemaBundleIdFromSchemaBundleName(String fullSchemaBundleName) { + Matcher matcher = SCHEMA_BUNDLE_PATTERN.matcher(fullSchemaBundleName); + if (!matcher.matches()) { + throw new IllegalArgumentException("Invalid schema bundle name: " + fullSchemaBundleName); + } + 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/CreateSchemaBundleRequest.java b/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/admin/v2/models/CreateSchemaBundleRequest.java new file mode 100644 index 000000000000..ea966d81c4bc --- /dev/null +++ b/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/admin/v2/models/CreateSchemaBundleRequest.java @@ -0,0 +1,109 @@ +/* + * 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.bigtable.admin.v2.ProtoSchema; +import com.google.cloud.bigtable.admin.v2.internal.NameUtil; +import com.google.common.base.Objects; +import com.google.common.base.Preconditions; +import com.google.protobuf.ByteString; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Paths; +import javax.annotation.Nonnull; + +/** + * Parameters for creating a new Cloud Bigtable {@link SchemaBundle}, which represents subsets of a + * particular table. + * + *

Sample code: + * + *

{@code
+ * CreateSchemaBundleRequest request =
+ *     CreateSchemaBundleRequest.of("my-table", "my-new-schema-bundle")
+ *         .setProtoSchemaFile("proto_file.pb");
+ * }
+ * + * @see SchemaBundle for more details. + */ +public final class CreateSchemaBundleRequest { + private final String tableId; + private final com.google.bigtable.admin.v2.CreateSchemaBundleRequest.Builder requestBuilder = + com.google.bigtable.admin.v2.CreateSchemaBundleRequest.newBuilder(); + + public static CreateSchemaBundleRequest of( + @Nonnull String tableId, @Nonnull String schemaBundleId) { + return new CreateSchemaBundleRequest(tableId, schemaBundleId); + } + + private CreateSchemaBundleRequest(@Nonnull String tableId, @Nonnull String schemaBundleId) { + Preconditions.checkNotNull(tableId, "tableId must be set"); + Preconditions.checkNotNull(schemaBundleId, "schemaBundleId must be set"); + + this.tableId = tableId; + requestBuilder.setSchemaBundleId(schemaBundleId); + } + + /** Sets the proto schema for this schema bundle. */ + public CreateSchemaBundleRequest setProtoSchemaFile(@Nonnull String protoSchemaFile) + throws IOException { + Preconditions.checkNotNull(protoSchemaFile, "protoSchemaFile must be set"); + byte[] content = Files.readAllBytes(Paths.get(protoSchemaFile)); + return setProtoSchema(ByteString.copyFrom(content)); + } + + /** Sets the proto schema for this schema bundle. */ + public CreateSchemaBundleRequest setProtoSchema(@Nonnull ByteString protoSchema) + throws IOException { + Preconditions.checkNotNull(protoSchema, "protoSchema must be set"); + requestBuilder.setSchemaBundle( + com.google.bigtable.admin.v2.SchemaBundle.newBuilder() + .setProtoSchema(ProtoSchema.newBuilder().setProtoDescriptors(protoSchema))); + return this; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + CreateSchemaBundleRequest that = (CreateSchemaBundleRequest) 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.CreateSchemaBundleRequest 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/SchemaBundle.java b/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/admin/v2/models/SchemaBundle.java new file mode 100644 index 000000000000..7782c335a2ef --- /dev/null +++ b/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/admin/v2/models/SchemaBundle.java @@ -0,0 +1,95 @@ +/* + * 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.bigtable.admin.v2.SchemaBundleName; +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.SchemaBundle} protocol buffer object. + */ +public final class SchemaBundle { + private final com.google.bigtable.admin.v2.SchemaBundle proto; + private final SchemaBundleName schemaBundleName; + + /** + * Wraps the protobuf. This method is considered an internal implementation detail and not meant + * to be used by applications. + */ + @InternalApi + public static SchemaBundle fromProto(@Nonnull com.google.bigtable.admin.v2.SchemaBundle proto) { + return new SchemaBundle(proto); + } + + private SchemaBundle(@Nonnull com.google.bigtable.admin.v2.SchemaBundle proto) { + Preconditions.checkNotNull(proto); + Preconditions.checkArgument(!proto.getName().isEmpty(), "SchemaBundle must have a name"); + Preconditions.checkArgument( + proto.hasProtoSchema(), "Schemabundle must have a proto_schema field"); + this.proto = proto; + this.schemaBundleName = SchemaBundleName.parse(proto.getName()); + } + + /** Gets the schema bundle's id. */ + public String getId() { + //noinspection ConstantConditions + return schemaBundleName.getSchemaBundle(); + } + + /** Gets the id of the table that owns this schema bundle. */ + public String getTableId() { + //noinspection ConstantConditions + return schemaBundleName.getTable(); + } + + /** Gets the proto schema of this schema bundle. */ + public com.google.protobuf.ByteString getProtoSchema() { + if (proto.hasProtoSchema()) { + return proto.getProtoSchema().getProtoDescriptors(); + } + throw new IllegalStateException("This SchemaBundle 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.SchemaBundle toProto() { + return proto; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + SchemaBundle that = (SchemaBundle) o; + return Objects.equal(proto, that.proto); + } + + @Override + public int hashCode() { + return Objects.hashCode(proto); + } +} diff --git a/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/admin/v2/models/UpdateSchemaBundleRequest.java b/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/admin/v2/models/UpdateSchemaBundleRequest.java new file mode 100644 index 000000000000..904c4d0097f4 --- /dev/null +++ b/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/admin/v2/models/UpdateSchemaBundleRequest.java @@ -0,0 +1,148 @@ +/* + * 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.bigtable.admin.v2.ProtoSchema; +import com.google.cloud.bigtable.admin.v2.internal.NameUtil; +import com.google.common.base.Objects; +import com.google.common.base.Preconditions; +import com.google.protobuf.ByteString; +import com.google.protobuf.FieldMask; +import com.google.protobuf.util.FieldMaskUtil; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Paths; +import javax.annotation.Nonnull; + +/** + * Parameters for updating an existing Cloud Bigtable {@link SchemaBundle}. + * + *

Sample code: + * + *

{@code
+ * SchemaBundle existingSchemaBundle = client.getSchemaBundle("my-table", "my-schema-bundle");
+ * UpdateSchemaBundleRequest request =
+ *     UpdateSchemaBundleRequest.of(existingSchemaBundle).setProtoSchemaFile("file.pb");
+ * }
+ * + * @see SchemaBundle for more details. + */ +public final class UpdateSchemaBundleRequest { + private final com.google.bigtable.admin.v2.UpdateSchemaBundleRequest.Builder requestBuilder; + private final String tableId; + private final String schemaBundleId; + + /** Builds a new update request using an existing schema bundle. */ + public static UpdateSchemaBundleRequest of(@Nonnull SchemaBundle schemaBundle) { + return new UpdateSchemaBundleRequest( + schemaBundle.getTableId(), + schemaBundle.getId(), + com.google.bigtable.admin.v2.UpdateSchemaBundleRequest.newBuilder() + .setSchemaBundle(schemaBundle.toProto())); + } + + /** Builds a new update schema bundle request. */ + public static UpdateSchemaBundleRequest of( + @Nonnull String tableId, @Nonnull String schemaBundleId) { + return new UpdateSchemaBundleRequest( + tableId, + schemaBundleId, + com.google.bigtable.admin.v2.UpdateSchemaBundleRequest.newBuilder()); + } + + private UpdateSchemaBundleRequest( + @Nonnull String tableId, + @Nonnull String schemaBundleId, + @Nonnull com.google.bigtable.admin.v2.UpdateSchemaBundleRequest.Builder requestBuilder) { + Preconditions.checkNotNull(tableId, "tableId must be set"); + Preconditions.checkNotNull(schemaBundleId, "schemaBundleId must be set"); + Preconditions.checkNotNull(requestBuilder, "proto builder must be set"); + + this.tableId = tableId; + this.schemaBundleId = schemaBundleId; + this.requestBuilder = requestBuilder; + } + + /** Sets the proto schema for this schema bundle. */ + public UpdateSchemaBundleRequest setProtoSchemaFile(@Nonnull String protoSchemaFile) + throws IOException { + Preconditions.checkNotNull(protoSchemaFile, "protoSchemaFile must be set"); + byte[] content = Files.readAllBytes(Paths.get(protoSchemaFile)); + return setProtoSchema(ByteString.copyFrom(content)); + } + + /** Sets the proto schema for this schema bundle. */ + public UpdateSchemaBundleRequest setProtoSchema(@Nonnull ByteString protoSchema) + throws IOException { + Preconditions.checkNotNull(protoSchema, "protoSchema must be set"); + requestBuilder.setSchemaBundle( + com.google.bigtable.admin.v2.SchemaBundle.newBuilder() + .setProtoSchema(ProtoSchema.newBuilder().setProtoDescriptors(protoSchema))); + updateFieldMask(com.google.bigtable.admin.v2.SchemaBundle.PROTO_SCHEMA_FIELD_NUMBER); + return this; + } + + /** + * Configures if safety warnings should be disabled. If set, then non backwards compatible changes + * are allowed. + */ + @SuppressWarnings("WeakerAccess") + public UpdateSchemaBundleRequest setIgnoreWarnings(boolean value) { + requestBuilder.setIgnoreWarnings(value); + return this; + } + + private void updateFieldMask(int fieldNumber) { + FieldMask newMask = + FieldMaskUtil.fromFieldNumbers( + com.google.bigtable.admin.v2.SchemaBundle.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; + } + UpdateSchemaBundleRequest that = (UpdateSchemaBundleRequest) o; + return Objects.equal(requestBuilder.build(), that.requestBuilder.build()) + && Objects.equal(tableId, that.tableId) + && Objects.equal(schemaBundleId, that.schemaBundleId); + } + + @Override + public int hashCode() { + return Objects.hashCode(requestBuilder.build(), tableId, schemaBundleId); + } + + /** + * 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.UpdateSchemaBundleRequest toProto( + @Nonnull String projectId, @Nonnull String instanceId) { + requestBuilder + .getSchemaBundleBuilder() + .setName(NameUtil.formatSchemaBundleName(projectId, instanceId, tableId, schemaBundleId)); + return requestBuilder.build(); + } +} 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 15713b17f193..0bf373116185 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 @@ -35,6 +35,7 @@ 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.CreateSchemaBundleMetadata; import com.google.bigtable.admin.v2.DeleteBackupRequest; import com.google.bigtable.admin.v2.DeleteTableRequest; import com.google.bigtable.admin.v2.DropRowRangeRequest; @@ -46,10 +47,12 @@ import com.google.bigtable.admin.v2.ModifyColumnFamiliesRequest.Modification; import com.google.bigtable.admin.v2.RestoreSourceType; import com.google.bigtable.admin.v2.RestoreTableMetadata; +import com.google.bigtable.admin.v2.SchemaBundleName; 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.UpdateSchemaBundleMetadata; import com.google.bigtable.admin.v2.UpdateTableMetadata; import com.google.cloud.Identity; import com.google.cloud.Policy; @@ -58,6 +61,8 @@ 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.ListSchemaBundlesPage; +import com.google.cloud.bigtable.admin.v2.BaseBigtableTableAdminClient.ListSchemaBundlesPagedResponse; 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; @@ -67,16 +72,19 @@ 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.CreateSchemaBundleRequest; 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.SchemaBundle; 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.models.UpdateSchemaBundleRequest; import com.google.cloud.bigtable.admin.v2.stub.EnhancedBigtableTableAdminStub; import com.google.common.collect.ImmutableList; import com.google.common.collect.Lists; @@ -90,6 +98,11 @@ import com.google.protobuf.util.Timestamps; import io.grpc.Status; import io.grpc.Status.Code; +import java.io.IOException; +import java.net.URISyntaxException; +import java.net.URL; +import java.nio.file.Files; +import java.nio.file.Paths; import java.util.List; import java.util.Map; import java.util.concurrent.ExecutionException; @@ -122,6 +135,11 @@ public class BigtableTableAdminClientTests { 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 SCHEMA_BUNDLE_ID = "my-schema-bundle"; + // Location: `google-cloud-bigtable/src/test/resources/proto_schema_bundle.pb` + private static final String TEST_PROTO_SCHEMA_BUNDLE = "proto_schema_bundle.pb"; + // Location: `google-cloud-bigtable/src/test/resources/updated_proto_schema_bundle.pb` + private static final String TEST_UPDATED_PROTO_SCHEMA_BUNDLE = "updated_proto_schema_bundle.pb"; private static final String INSTANCE_NAME = NameUtil.formatInstanceName(PROJECT_ID, INSTANCE_ID); private static final String TABLE_NAME = @@ -225,6 +243,35 @@ public class BigtableTableAdminClientTests { private UnaryCallable mockDeleteAuthorizedViewCallable; + @Mock + private OperationCallable< + com.google.bigtable.admin.v2.CreateSchemaBundleRequest, + com.google.bigtable.admin.v2.SchemaBundle, + CreateSchemaBundleMetadata> + mockCreateSchemaBundleOperationCallable; + + @Mock + private OperationCallable< + com.google.bigtable.admin.v2.UpdateSchemaBundleRequest, + com.google.bigtable.admin.v2.SchemaBundle, + UpdateSchemaBundleMetadata> + mockUpdateSchemaBundleOperationCallable; + + @Mock + private UnaryCallable< + com.google.bigtable.admin.v2.GetSchemaBundleRequest, + com.google.bigtable.admin.v2.SchemaBundle> + mockGetSchemaBundleCallable; + + @Mock + private UnaryCallable< + com.google.bigtable.admin.v2.ListSchemaBundlesRequest, ListSchemaBundlesPagedResponse> + mockListSchemaBundlesCallable; + + @Mock + private UnaryCallable + mockDeleteSchemaBundleCallable; + @Mock private UnaryCallable mockGetIamPolicyCallable; @@ -1291,6 +1338,218 @@ public void testDeleteAuthorizedView() { assertThat(wasCalled.get()).isTrue(); } + @Test + public void testCreateSchemaBundle() throws IOException, URISyntaxException { + // Setup + Mockito.when(mockStub.createSchemaBundleOperationCallable()) + .thenReturn(mockCreateSchemaBundleOperationCallable); + byte[] content = Files.readAllBytes(Paths.get(getResourceFilePath(TEST_PROTO_SCHEMA_BUNDLE))); + + com.google.bigtable.admin.v2.CreateSchemaBundleRequest expectedRequest = + com.google.bigtable.admin.v2.CreateSchemaBundleRequest.newBuilder() + .setParent(NameUtil.formatTableName(PROJECT_ID, INSTANCE_ID, TABLE_ID)) + .setSchemaBundleId(SCHEMA_BUNDLE_ID) + .setSchemaBundle( + com.google.bigtable.admin.v2.SchemaBundle.newBuilder() + .setProtoSchema( + com.google.bigtable.admin.v2.ProtoSchema.newBuilder() + .setProtoDescriptors(ByteString.copyFrom(content)))) + .build(); + + com.google.bigtable.admin.v2.SchemaBundle expectedResponse = + com.google.bigtable.admin.v2.SchemaBundle.newBuilder() + .setName( + NameUtil.formatSchemaBundleName( + PROJECT_ID, INSTANCE_ID, TABLE_ID, SCHEMA_BUNDLE_ID)) + .setProtoSchema( + com.google.bigtable.admin.v2.ProtoSchema.newBuilder() + .setProtoDescriptors(ByteString.copyFrom(content))) + .build(); + + mockOperationResult( + mockCreateSchemaBundleOperationCallable, + expectedRequest, + expectedResponse, + CreateSchemaBundleMetadata.newBuilder() + .setName(expectedRequest.getSchemaBundle().getName()) + .build()); + + CreateSchemaBundleRequest req = + CreateSchemaBundleRequest.of(TABLE_ID, SCHEMA_BUNDLE_ID) + .setProtoSchemaFile(getResourceFilePath(TEST_PROTO_SCHEMA_BUNDLE)); + + // Execute + SchemaBundle actualResult = adminClient.createSchemaBundle(req); + + // Verify + assertThat(actualResult).isEqualTo(SchemaBundle.fromProto(expectedResponse)); + } + + @Test + public void testUpdateSchemaBundle() throws IOException, URISyntaxException { + // Setup + Mockito.when(mockStub.updateSchemaBundleOperationCallable()) + .thenReturn(mockUpdateSchemaBundleOperationCallable); + byte[] content = + Files.readAllBytes(Paths.get(getResourceFilePath(TEST_UPDATED_PROTO_SCHEMA_BUNDLE))); + + com.google.bigtable.admin.v2.UpdateSchemaBundleRequest expectedRequest = + com.google.bigtable.admin.v2.UpdateSchemaBundleRequest.newBuilder() + .setSchemaBundle( + com.google.bigtable.admin.v2.SchemaBundle.newBuilder() + .setName( + NameUtil.formatSchemaBundleName( + PROJECT_ID, INSTANCE_ID, TABLE_ID, SCHEMA_BUNDLE_ID)) + .setProtoSchema( + com.google.bigtable.admin.v2.ProtoSchema.newBuilder() + .setProtoDescriptors(ByteString.copyFrom(content))) + .build()) + .setUpdateMask(FieldMask.newBuilder().addPaths("proto_schema")) + .build(); + + com.google.bigtable.admin.v2.SchemaBundle expectedResponse = + com.google.bigtable.admin.v2.SchemaBundle.newBuilder() + .setName( + NameUtil.formatSchemaBundleName( + PROJECT_ID, INSTANCE_ID, TABLE_ID, SCHEMA_BUNDLE_ID)) + .setProtoSchema( + com.google.bigtable.admin.v2.ProtoSchema.newBuilder() + .setProtoDescriptors(ByteString.copyFrom(content))) + .build(); + + mockOperationResult( + mockUpdateSchemaBundleOperationCallable, + expectedRequest, + expectedResponse, + UpdateSchemaBundleMetadata.newBuilder() + .setName(expectedRequest.getSchemaBundle().getName()) + .build()); + + UpdateSchemaBundleRequest req = + UpdateSchemaBundleRequest.of(TABLE_ID, SCHEMA_BUNDLE_ID) + .setProtoSchemaFile(getResourceFilePath(TEST_UPDATED_PROTO_SCHEMA_BUNDLE)); + + // Execute + SchemaBundle actualResult = adminClient.updateSchemaBundle(req); + + // Verify + assertThat(actualResult).isEqualTo(SchemaBundle.fromProto(expectedResponse)); + } + + @Test + public void testGetSchemaBundle() { + // Setup + Mockito.when(mockStub.getSchemaBundleCallable()).thenReturn(mockGetSchemaBundleCallable); + + com.google.bigtable.admin.v2.GetSchemaBundleRequest expectedRequest = + com.google.bigtable.admin.v2.GetSchemaBundleRequest.newBuilder() + .setName( + NameUtil.formatSchemaBundleName( + PROJECT_ID, INSTANCE_ID, TABLE_ID, SCHEMA_BUNDLE_ID)) + .build(); + + com.google.bigtable.admin.v2.SchemaBundle expectedResponse = + com.google.bigtable.admin.v2.SchemaBundle.newBuilder() + .setName( + NameUtil.formatSchemaBundleName( + PROJECT_ID, INSTANCE_ID, TABLE_ID, SCHEMA_BUNDLE_ID)) + .setProtoSchema( + com.google.bigtable.admin.v2.ProtoSchema.newBuilder() + .setProtoDescriptors(ByteString.copyFromUtf8("schema"))) + .build(); + + Mockito.when(mockGetSchemaBundleCallable.futureCall(expectedRequest)) + .thenReturn(ApiFutures.immediateFuture(expectedResponse)); + + // Execute + SchemaBundle actualResult = adminClient.getSchemaBundle(TABLE_ID, SCHEMA_BUNDLE_ID); + + // Verify + assertThat(actualResult).isEqualTo(SchemaBundle.fromProto(expectedResponse)); + } + + @Test + public void testListSchemaBundles() { + // Setup + Mockito.when(mockStub.listSchemaBundlesPagedCallable()) + .thenReturn(mockListSchemaBundlesCallable); + + com.google.bigtable.admin.v2.ListSchemaBundlesRequest expectedRequest = + com.google.bigtable.admin.v2.ListSchemaBundlesRequest.newBuilder() + .setParent(NameUtil.formatTableName(PROJECT_ID, INSTANCE_ID, TABLE_ID)) + .build(); + + // 3 SchemaBundles spread across 2 pages + List expectedProtos = Lists.newArrayList(); + for (int i = 0; i < 3; i++) { + expectedProtos.add( + com.google.bigtable.admin.v2.SchemaBundle.newBuilder() + .setName( + NameUtil.formatSchemaBundleName( + PROJECT_ID, INSTANCE_ID, TABLE_ID, SCHEMA_BUNDLE_ID + i)) + .build()); + } + + // 2 on the first page + ListSchemaBundlesPage page0 = Mockito.mock(ListSchemaBundlesPage.class); + Mockito.when(page0.getValues()).thenReturn(expectedProtos.subList(0, 2)); + Mockito.when(page0.hasNextPage()).thenReturn(true); + + // 1 on the last page + ListSchemaBundlesPage page1 = Mockito.mock(ListSchemaBundlesPage.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 + ListSchemaBundlesPagedResponse response0 = Mockito.mock(ListSchemaBundlesPagedResponse.class); + Mockito.when(response0.getPage()).thenReturn(page0); + + Mockito.when(mockListSchemaBundlesCallable.futureCall(expectedRequest)) + .thenReturn(ApiFutures.immediateFuture(response0)); + + // Execute + List actualResults = adminClient.listSchemaBundles(TABLE_ID); + + // Verify + List expectedResults = Lists.newArrayList(); + for (com.google.bigtable.admin.v2.SchemaBundle expectedProto : expectedProtos) { + expectedResults.add(SchemaBundleName.parse(expectedProto.getName()).getSchemaBundle()); + } + + assertThat(actualResults).containsExactlyElementsIn(expectedResults); + } + + @Test + public void testDeleteSchemaBundle() { + // Setup + Mockito.when(mockStub.deleteSchemaBundleCallable()).thenReturn(mockDeleteSchemaBundleCallable); + + com.google.bigtable.admin.v2.DeleteSchemaBundleRequest expectedRequest = + com.google.bigtable.admin.v2.DeleteSchemaBundleRequest.newBuilder() + .setName( + NameUtil.formatSchemaBundleName( + PROJECT_ID, INSTANCE_ID, TABLE_ID, SCHEMA_BUNDLE_ID)) + .build(); + + final AtomicBoolean wasCalled = new AtomicBoolean(false); + + Mockito.when(mockDeleteSchemaBundleCallable.futureCall(expectedRequest)) + .thenAnswer( + (Answer>) + invocationOnMock -> { + wasCalled.set(true); + return ApiFutures.immediateFuture(Empty.getDefaultInstance()); + }); + + // Execute + adminClient.deleteSchemaBundle(TABLE_ID, SCHEMA_BUNDLE_ID); + + // Verify + assertThat(wasCalled.get()).isTrue(); + } + @Test public void testGetBackupIamPolicy() { // Setup @@ -1415,4 +1674,10 @@ private void mockOperationResult( OperationFutures.immediateOperationFuture(operationSnapshot); Mockito.when(callable.futureCall(request)).thenReturn(operationFuture); } + + private String getResourceFilePath(String filePath) throws URISyntaxException { + ClassLoader cl = Thread.currentThread().getContextClassLoader(); + URL protoSchema = cl.getResource(filePath); + return Paths.get(protoSchema.toURI()).toAbsolutePath().toString(); + } } 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 530077721db5..68f0b590d896 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 @@ -60,6 +60,17 @@ public void formatAuthorizedViewNameTest() { .isEqualTo(testAuthorizedViewName); } + @Test + public void formatSchemabundleNameTest() { + String testSchemaBundleName = + "projects/my-project/instances/my-instance/tables/my-table/schemaBundles/my-schema-bundle"; + + assertThat( + NameUtil.formatSchemaBundleName( + "my-project", "my-instance", "my-table", "my-schema-bundle")) + .isEqualTo(testSchemaBundleName); + } + @Test public void formatMaterializedViewNameTest() { String testMaterializedViewName = @@ -92,6 +103,18 @@ public void extractAuthorizedViewIdFromAuthorizedViewNameTest() { NameUtil.extractAuthorizedViewIdFromAuthorizedViewName("bad-format"); } + @Test + public void extractSchemaBundleIdFromSchemaBundleNameTest() { + String testSchemaBundleName = + "projects/my-project/instances/my-instance/tables/my-table/schemaBundles/my-schema-bundle"; + + assertThat(NameUtil.extractSchemaBundleIdFromSchemaBundleName(testSchemaBundleName)) + .isEqualTo("my-schema-bundle"); + + exception.expect(IllegalArgumentException.class); + NameUtil.extractSchemaBundleIdFromSchemaBundleName("bad-format"); + } + @Test public void extractTableIdFromAuthorizedViewNameTest() { String testAuthorizedViewName = diff --git a/google-cloud-bigtable/src/test/java/com/google/cloud/bigtable/admin/v2/it/BigtableSchemaBundleIT.java b/google-cloud-bigtable/src/test/java/com/google/cloud/bigtable/admin/v2/it/BigtableSchemaBundleIT.java new file mode 100644 index 000000000000..62001f8230d9 --- /dev/null +++ b/google-cloud-bigtable/src/test/java/com/google/cloud/bigtable/admin/v2/it/BigtableSchemaBundleIT.java @@ -0,0 +1,219 @@ +/* + * 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.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.NotFoundException; +import com.google.cloud.bigtable.admin.v2.BigtableTableAdminClient; +import com.google.cloud.bigtable.admin.v2.models.CreateSchemaBundleRequest; +import com.google.cloud.bigtable.admin.v2.models.CreateTableRequest; +import com.google.cloud.bigtable.admin.v2.models.SchemaBundle; +import com.google.cloud.bigtable.admin.v2.models.Table; +import com.google.cloud.bigtable.admin.v2.models.UpdateSchemaBundleRequest; +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.io.IOException; +import java.net.URISyntaxException; +import java.net.URL; +import java.nio.file.Files; +import java.nio.file.Paths; +import java.util.List; +import java.util.Random; +import java.util.logging.Logger; +import org.junit.After; +import org.junit.Before; +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 BigtableSchemaBundleIT { + @ClassRule public static final TestEnvRule testEnvRule = new TestEnvRule(); + @Rule public final PrefixGenerator prefixGenerator = new PrefixGenerator(); + private static final Logger LOGGER = Logger.getLogger(BigtableSchemaBundleIT.class.getName()); + private static final int[] BACKOFF_DURATION = {2, 4, 8, 16, 32, 64, 128, 256, 512, 1024}; + // Location: `google-cloud-bigtable/src/test/resources/proto_schema_bundle.pb` + private static final String TEST_PROTO_SCHEMA_BUNDLE = "proto_schema_bundle.pb"; + // Location: + // `google-cloud-bigtable/src/test/resources/updated_proto_schema_bundle.pb` + private static final String TEST_UPDATED_PROTO_SCHEMA_BUNDLE = "updated_proto_schema_bundle.pb"; + + private static BigtableTableAdminClient tableAdmin; + private static BigtableDataClient dataClient; + private 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(); + } + + @Before + public void setUp() throws InterruptedException { + testTable = createAndPopulateTestTable(tableAdmin, dataClient); + } + + @After + public void tearDown() { + if (testTable != null) { + try { + tableAdmin.deleteTable(testTable.getId()); + } catch (Exception e) { + // Ignore. + } + } + } + + @Test + public void createSchemaBundleAndGetSchemaBundleTest() throws IOException, URISyntaxException { + String SchemaBundleId = prefixGenerator.newPrefix(); + byte[] content = Files.readAllBytes(Paths.get(getResourceFilePath(TEST_PROTO_SCHEMA_BUNDLE))); + + CreateSchemaBundleRequest request = + CreateSchemaBundleRequest.of(testTable.getId(), SchemaBundleId) + .setProtoSchemaFile(getResourceFilePath(TEST_PROTO_SCHEMA_BUNDLE)); + try { + SchemaBundle response = tableAdmin.createSchemaBundle(request); + assertWithMessage("Got wrong schema bundle Id in createSchemaBundle") + .that(response.getId()) + .isEqualTo(SchemaBundleId); + assertWithMessage("Got wrong proto schema in createSchemaBundle") + .that(response.getProtoSchema()) + .isEqualTo(ByteString.copyFrom(content)); + + response = tableAdmin.getSchemaBundle(testTable.getId(), SchemaBundleId); + assertWithMessage("Got wrong schema bundle Id in getSchemaBundle") + .that(response.getId()) + .isEqualTo(SchemaBundleId); + assertWithMessage("Got wrong proto schema in getSchemaBundle") + .that(response.getProtoSchema()) + .isEqualTo(ByteString.copyFrom(content)); + } finally { + tableAdmin.deleteSchemaBundle(testTable.getId(), SchemaBundleId); + } + } + + @Test + public void listSchemaBundlesTest() throws IOException, URISyntaxException { + String SchemaBundleId1 = prefixGenerator.newPrefix(); + String SchemaBundleId2 = prefixGenerator.newPrefix(); + + tableAdmin.createSchemaBundle(createSchemaBundleRequest(SchemaBundleId1)); + tableAdmin.createSchemaBundle(createSchemaBundleRequest(SchemaBundleId2)); + + List response = tableAdmin.listSchemaBundles(testTable.getId()); + // Concurrent tests running may cause flakiness. Use containsAtLeast instead of + // containsExactly. + assertWithMessage("Got wrong schema bundle Ids in listSchemaBundles") + .that(response) + .containsAtLeast( + tableAdmin.getSchemaBundle(testTable.getId(), SchemaBundleId1).getId(), + tableAdmin.getSchemaBundle(testTable.getId(), SchemaBundleId2).getId()); + } + + @Test + public void updateSchemaBundleAndDeleteSchemaBundleTest() + throws InterruptedException, IOException, URISyntaxException { + String SchemaBundleId = prefixGenerator.newPrefix(); + + // Create a schema bundle. + CreateSchemaBundleRequest request = createSchemaBundleRequest(SchemaBundleId); + + SchemaBundle response = tableAdmin.createSchemaBundle(request); + + // Update the schema bundle. + byte[] content = + Files.readAllBytes(Paths.get(getResourceFilePath(TEST_UPDATED_PROTO_SCHEMA_BUNDLE))); + UpdateSchemaBundleRequest updateRequest = + UpdateSchemaBundleRequest.of(testTable.getId(), SchemaBundleId) + .setProtoSchemaFile(getResourceFilePath(TEST_UPDATED_PROTO_SCHEMA_BUNDLE)); + response = tableAdmin.updateSchemaBundle(updateRequest); + assertWithMessage("Got wrong deletion protection in UpdateSchemaBundle") + .that(response.getProtoSchema()) + .isEqualTo(ByteString.copyFrom(content)); + + // Now we should be able to successfully delete the SchemaBundle. + tableAdmin.deleteSchemaBundle(testTable.getId(), SchemaBundleId); + try { + for (int i = 0; i < BACKOFF_DURATION.length; i++) { + tableAdmin.getSchemaBundle(testTable.getId(), SchemaBundleId); + + LOGGER.info( + "Wait for " + + BACKOFF_DURATION[i] + + " seconds for deleting schema bundle " + + SchemaBundleId); + Thread.sleep(BACKOFF_DURATION[i] * 1000); + } + fail("SchemaBundle was not deleted."); + } catch (NotFoundException e) { + assertWithMessage("Incorrect exception type") + .that(e.getCause()) + .isInstanceOf(StatusRuntimeException.class); + } + } + + private CreateSchemaBundleRequest createSchemaBundleRequest(String SchemaBundleId) + throws IOException, URISyntaxException { + return CreateSchemaBundleRequest.of(testTable.getId(), SchemaBundleId) + .setProtoSchemaFile(getResourceFilePath(TEST_PROTO_SCHEMA_BUNDLE)); + } + + private static Table createAndPopulateTestTable( + BigtableTableAdminClient tableAdmin, BigtableDataClient dataClient) + throws InterruptedException { + String tableId = PrefixGenerator.newPrefix("BigtableSchemaBundleIT#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; + } + + private String getResourceFilePath(String filePath) throws URISyntaxException { + ClassLoader cl = Thread.currentThread().getContextClassLoader(); + URL protoSchema = cl.getResource(filePath); + return Paths.get(protoSchema.toURI()).toAbsolutePath().toString(); + } +} diff --git a/google-cloud-bigtable/src/test/java/com/google/cloud/bigtable/admin/v2/models/CreateSchemaBundleRequestTest.java b/google-cloud-bigtable/src/test/java/com/google/cloud/bigtable/admin/v2/models/CreateSchemaBundleRequestTest.java new file mode 100644 index 000000000000..2d37eccff5b0 --- /dev/null +++ b/google-cloud-bigtable/src/test/java/com/google/cloud/bigtable/admin/v2/models/CreateSchemaBundleRequestTest.java @@ -0,0 +1,107 @@ +/* + * 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 java.io.IOException; +import java.net.URISyntaxException; +import java.net.URL; +import java.nio.file.Files; +import java.nio.file.Paths; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + +@RunWith(JUnit4.class) +public class CreateSchemaBundleRequestTest { + 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 SCHEMA_BUNDLE_ID = "my-schema-bundle"; + // Location: `google-cloud-bigtable/src/test/resources/proto_schema_bundle.pb` + private static final String TEST_PROTO_SCHEMA_BUNDLE = "proto_schema_bundle.pb"; + // Location: `google-cloud-bigtable/src/test/resources/updated_proto_schema_bundle.pb` + private static final String TEST_UPDATED_PROTO_SCHEMA_BUNDLE = "updated_proto_schema_bundle.pb"; + + @Test + public void testToProto() throws IOException, URISyntaxException { + CreateSchemaBundleRequest request = + CreateSchemaBundleRequest.of(TABLE_ID, SCHEMA_BUNDLE_ID) + .setProtoSchemaFile(getResourceFilePath(TEST_PROTO_SCHEMA_BUNDLE)); + + byte[] content = Files.readAllBytes(Paths.get(getResourceFilePath(TEST_PROTO_SCHEMA_BUNDLE))); + + com.google.bigtable.admin.v2.CreateSchemaBundleRequest requestProto = + com.google.bigtable.admin.v2.CreateSchemaBundleRequest.newBuilder() + .setParent(NameUtil.formatTableName(PROJECT_ID, INSTANCE_ID, TABLE_ID)) + .setSchemaBundleId(SCHEMA_BUNDLE_ID) + .setSchemaBundle( + com.google.bigtable.admin.v2.SchemaBundle.newBuilder() + .setProtoSchema( + com.google.bigtable.admin.v2.ProtoSchema.newBuilder() + .setProtoDescriptors(ByteString.copyFrom(content)) + .build()) + .build()) + .build(); + assertThat(request.toProto(PROJECT_ID, INSTANCE_ID)).isEqualTo(requestProto); + } + + @Test + public void testEquality() throws IOException, URISyntaxException { + CreateSchemaBundleRequest request = + CreateSchemaBundleRequest.of(TABLE_ID, SCHEMA_BUNDLE_ID) + .setProtoSchemaFile(getResourceFilePath(TEST_PROTO_SCHEMA_BUNDLE)); + + assertThat(request) + .isEqualTo( + CreateSchemaBundleRequest.of(TABLE_ID, SCHEMA_BUNDLE_ID) + .setProtoSchemaFile(getResourceFilePath(TEST_PROTO_SCHEMA_BUNDLE))); + + assertThat(request) + .isNotEqualTo( + CreateSchemaBundleRequest.of(TABLE_ID, SCHEMA_BUNDLE_ID) + .setProtoSchemaFile(getResourceFilePath(TEST_UPDATED_PROTO_SCHEMA_BUNDLE))); + } + + @Test + public void testHashCode() throws IOException, URISyntaxException { + CreateSchemaBundleRequest request = + CreateSchemaBundleRequest.of(TABLE_ID, SCHEMA_BUNDLE_ID) + .setProtoSchemaFile(getResourceFilePath(TEST_PROTO_SCHEMA_BUNDLE)); + + assertThat(request.hashCode()) + .isEqualTo( + CreateSchemaBundleRequest.of(TABLE_ID, SCHEMA_BUNDLE_ID) + .setProtoSchemaFile(getResourceFilePath(TEST_PROTO_SCHEMA_BUNDLE)) + .hashCode()); + + assertThat(request.hashCode()) + .isNotEqualTo( + CreateSchemaBundleRequest.of(TABLE_ID, SCHEMA_BUNDLE_ID) + .setProtoSchemaFile(getResourceFilePath(TEST_UPDATED_PROTO_SCHEMA_BUNDLE)) + .hashCode()); + } + + private String getResourceFilePath(String filePath) throws URISyntaxException { + ClassLoader cl = Thread.currentThread().getContextClassLoader(); + URL protoSchema = cl.getResource(filePath); + return Paths.get(protoSchema.toURI()).toAbsolutePath().toString(); + } +} diff --git a/google-cloud-bigtable/src/test/java/com/google/cloud/bigtable/admin/v2/models/SchemaBundleTest.java b/google-cloud-bigtable/src/test/java/com/google/cloud/bigtable/admin/v2/models/SchemaBundleTest.java new file mode 100644 index 000000000000..09f675aa63af --- /dev/null +++ b/google-cloud-bigtable/src/test/java/com/google/cloud/bigtable/admin/v2/models/SchemaBundleTest.java @@ -0,0 +1,148 @@ +/* + * 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.SchemaBundleName; +import com.google.protobuf.ByteString; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + +@RunWith(JUnit4.class) +public class SchemaBundleTest { + 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 SCHEMA_BUNDLE_ID = "my-schema-bundle"; + + @Test + public void testFromProto() { + SchemaBundleName schemaBundleName = + SchemaBundleName.of(PROJECT_ID, INSTANCE_ID, TABLE_ID, SCHEMA_BUNDLE_ID); + + com.google.bigtable.admin.v2.SchemaBundle schemaBundleProto = + com.google.bigtable.admin.v2.SchemaBundle.newBuilder() + .setName(schemaBundleName.toString()) + .setProtoSchema( + com.google.bigtable.admin.v2.ProtoSchema.newBuilder() + .setProtoDescriptors(ByteString.copyFromUtf8("schema")) + .build()) + .build(); + + SchemaBundle result = SchemaBundle.fromProto(schemaBundleProto); + + assertThat(result.getId()).isEqualTo(SCHEMA_BUNDLE_ID); + assertThat(result.getTableId()).isEqualTo(TABLE_ID); + assertThat(result.getProtoSchema()).isEqualTo(ByteString.copyFromUtf8("schema")); + } + + @Test + public void testRequiresName() { + com.google.bigtable.admin.v2.SchemaBundle proto = + com.google.bigtable.admin.v2.SchemaBundle.newBuilder() + .setProtoSchema( + com.google.bigtable.admin.v2.ProtoSchema.newBuilder() + .setProtoDescriptors(ByteString.copyFromUtf8("schema")) + .build()) + .build(); + Exception actualException = null; + + try { + SchemaBundle.fromProto(proto); + } catch (Exception e) { + actualException = e; + } + + assertThat(actualException).isInstanceOf(IllegalArgumentException.class); + } + + @Test + public void testRequiresSchemaBundleType() { + SchemaBundleName schemaBundleName = + SchemaBundleName.of(PROJECT_ID, INSTANCE_ID, TABLE_ID, SCHEMA_BUNDLE_ID); + com.google.bigtable.admin.v2.SchemaBundle proto = + com.google.bigtable.admin.v2.SchemaBundle.newBuilder() + .setName(schemaBundleName.toString()) + .build(); + Exception actualException = null; + + try { + SchemaBundle.fromProto(proto); + } catch (Exception e) { + actualException = e; + } + + assertThat(actualException).isInstanceOf(IllegalArgumentException.class); + } + + @Test + public void testEquality() { + SchemaBundleName schemaBundleName = + SchemaBundleName.of(PROJECT_ID, INSTANCE_ID, TABLE_ID, SCHEMA_BUNDLE_ID); + com.google.bigtable.admin.v2.SchemaBundle proto = + com.google.bigtable.admin.v2.SchemaBundle.newBuilder() + .setName(schemaBundleName.toString()) + .setProtoSchema( + com.google.bigtable.admin.v2.ProtoSchema.newBuilder() + .setProtoDescriptors(ByteString.copyFromUtf8("schema")) + .build()) + .build(); + SchemaBundle schemaBundle = SchemaBundle.fromProto(proto); + + assertThat(schemaBundle).isEqualTo(SchemaBundle.fromProto(proto)); + + assertThat(schemaBundle) + .isNotEqualTo( + com.google.bigtable.admin.v2.SchemaBundle.newBuilder() + .setName(schemaBundleName.toString()) + .setProtoSchema( + com.google.bigtable.admin.v2.ProtoSchema.newBuilder() + .setProtoDescriptors(ByteString.copyFromUtf8("schema")) + .build()) + .build()); + } + + @Test + public void testHashCode() { + SchemaBundleName schemaBundleName = + SchemaBundleName.of(PROJECT_ID, INSTANCE_ID, TABLE_ID, SCHEMA_BUNDLE_ID); + com.google.bigtable.admin.v2.SchemaBundle proto = + com.google.bigtable.admin.v2.SchemaBundle.newBuilder() + .setName(schemaBundleName.toString()) + .setProtoSchema( + com.google.bigtable.admin.v2.ProtoSchema.newBuilder() + .setProtoDescriptors(ByteString.copyFromUtf8("schema")) + .build()) + .build(); + SchemaBundle schemaBundle = SchemaBundle.fromProto(proto); + + assertThat(schemaBundle.hashCode()).isEqualTo(SchemaBundle.fromProto(proto).hashCode()); + + assertThat(schemaBundle.hashCode()) + .isNotEqualTo( + com.google.bigtable.admin.v2.SchemaBundle.newBuilder() + .setName(schemaBundleName.toString()) + .setProtoSchema( + com.google.bigtable.admin.v2.ProtoSchema.newBuilder() + .setProtoDescriptors(ByteString.copyFromUtf8("schema")) + .build()) + .build() + .hashCode()); + } +} diff --git a/google-cloud-bigtable/src/test/java/com/google/cloud/bigtable/admin/v2/models/UpdateSchemaBundleRequestTest.java b/google-cloud-bigtable/src/test/java/com/google/cloud/bigtable/admin/v2/models/UpdateSchemaBundleRequestTest.java new file mode 100644 index 000000000000..994d56068aad --- /dev/null +++ b/google-cloud-bigtable/src/test/java/com/google/cloud/bigtable/admin/v2/models/UpdateSchemaBundleRequestTest.java @@ -0,0 +1,145 @@ +/* + * 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.ProtoSchema; +import com.google.cloud.bigtable.admin.v2.internal.NameUtil; +import com.google.protobuf.ByteString; +import com.google.protobuf.FieldMask; +import java.io.IOException; +import java.net.URISyntaxException; +import java.net.URL; +import java.nio.file.Files; +import java.nio.file.Paths; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + +@RunWith(JUnit4.class) +public class UpdateSchemaBundleRequestTest { + 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 SCHEMA_BUNDLE_ID = "my-schema-bundle"; + // Location: `google-cloud-bigtable/src/test/resources/proto_schema_bundle.pb` + private static final String TEST_PROTO_SCHEMA_BUNDLE = "proto_schema_bundle.pb"; + // Location: `google-cloud-bigtable/src/test/resources/updated_proto_schema_bundle.pb` + private static final String TEST_UPDATED_PROTO_SCHEMA_BUNDLE = "updated_proto_schema_bundle.pb"; + + @Test + public void testToProto() throws IOException, URISyntaxException { + UpdateSchemaBundleRequest request = + UpdateSchemaBundleRequest.of(TABLE_ID, SCHEMA_BUNDLE_ID) + .setProtoSchemaFile(getResourceFilePath(TEST_PROTO_SCHEMA_BUNDLE)) + .setIgnoreWarnings(true); + byte[] content = Files.readAllBytes(Paths.get(getResourceFilePath(TEST_PROTO_SCHEMA_BUNDLE))); + + com.google.bigtable.admin.v2.UpdateSchemaBundleRequest requestProto = + com.google.bigtable.admin.v2.UpdateSchemaBundleRequest.newBuilder() + .setSchemaBundle( + com.google.bigtable.admin.v2.SchemaBundle.newBuilder() + .setName( + NameUtil.formatSchemaBundleName( + PROJECT_ID, INSTANCE_ID, TABLE_ID, SCHEMA_BUNDLE_ID)) + .setProtoSchema( + com.google.bigtable.admin.v2.ProtoSchema.newBuilder() + .setProtoDescriptors(ByteString.copyFrom(content)) + .build()) + .build()) + .setUpdateMask(FieldMask.newBuilder().addPaths("proto_schema")) + .setIgnoreWarnings(true) + .build(); + assertThat(request.toProto(PROJECT_ID, INSTANCE_ID)).isEqualTo(requestProto); + } + + @Test + public void testUpdateProtoSchema() throws IOException, URISyntaxException { + byte[] content = Files.readAllBytes(Paths.get(getResourceFilePath(TEST_PROTO_SCHEMA_BUNDLE))); + byte[] updated_content = + Files.readAllBytes(Paths.get(getResourceFilePath(TEST_UPDATED_PROTO_SCHEMA_BUNDLE))); + + com.google.bigtable.admin.v2.SchemaBundle existingSchemaBundle = + com.google.bigtable.admin.v2.SchemaBundle.newBuilder() + .setName( + NameUtil.formatSchemaBundleName( + PROJECT_ID, INSTANCE_ID, TABLE_ID, SCHEMA_BUNDLE_ID)) + .setProtoSchema( + com.google.bigtable.admin.v2.ProtoSchema.newBuilder() + .setProtoDescriptors(ByteString.copyFrom(content)) + .build()) + .build(); + + UpdateSchemaBundleRequest request = + UpdateSchemaBundleRequest.of(SchemaBundle.fromProto(existingSchemaBundle)) + .setProtoSchemaFile(getResourceFilePath(TEST_UPDATED_PROTO_SCHEMA_BUNDLE)); + + com.google.bigtable.admin.v2.UpdateSchemaBundleRequest requestProto = + com.google.bigtable.admin.v2.UpdateSchemaBundleRequest.newBuilder() + .setSchemaBundle( + existingSchemaBundle.toBuilder() + .setProtoSchema( + ProtoSchema.newBuilder() + .setProtoDescriptors(ByteString.copyFrom(updated_content)))) + .setUpdateMask(FieldMask.newBuilder().addPaths("proto_schema")) + .build(); + assertThat(request.toProto(PROJECT_ID, INSTANCE_ID)).isEqualTo(requestProto); + } + + @Test + public void testEquality() throws IOException, URISyntaxException { + UpdateSchemaBundleRequest request = + UpdateSchemaBundleRequest.of(TABLE_ID, SCHEMA_BUNDLE_ID) + .setProtoSchemaFile(getResourceFilePath(TEST_PROTO_SCHEMA_BUNDLE)); + + assertThat(request) + .isEqualTo( + UpdateSchemaBundleRequest.of(TABLE_ID, SCHEMA_BUNDLE_ID) + .setProtoSchemaFile(getResourceFilePath(TEST_PROTO_SCHEMA_BUNDLE))); + + assertThat(request) + .isNotEqualTo( + UpdateSchemaBundleRequest.of(TABLE_ID, SCHEMA_BUNDLE_ID) + .setProtoSchemaFile(getResourceFilePath(TEST_UPDATED_PROTO_SCHEMA_BUNDLE))); + } + + @Test + public void testHashCode() throws IOException, URISyntaxException { + UpdateSchemaBundleRequest request = + UpdateSchemaBundleRequest.of(TABLE_ID, SCHEMA_BUNDLE_ID) + .setProtoSchemaFile(getResourceFilePath(TEST_PROTO_SCHEMA_BUNDLE)); + + assertThat(request.hashCode()) + .isEqualTo( + UpdateSchemaBundleRequest.of(TABLE_ID, SCHEMA_BUNDLE_ID) + .setProtoSchemaFile(getResourceFilePath(TEST_PROTO_SCHEMA_BUNDLE)) + .hashCode()); + + assertThat(request.hashCode()) + .isNotEqualTo( + UpdateSchemaBundleRequest.of(TABLE_ID, SCHEMA_BUNDLE_ID) + .setProtoSchemaFile(getResourceFilePath(TEST_UPDATED_PROTO_SCHEMA_BUNDLE)) + .hashCode()); + } + + private String getResourceFilePath(String filePath) throws URISyntaxException { + ClassLoader cl = Thread.currentThread().getContextClassLoader(); + URL protoSchema = cl.getResource(filePath); + return Paths.get(protoSchema.toURI()).toAbsolutePath().toString(); + } +} diff --git a/google-cloud-bigtable/src/test/resources/proto_schema_bundle.pb b/google-cloud-bigtable/src/test/resources/proto_schema_bundle.pb new file mode 100644 index 000000000000..c9ac4086f5d7 --- /dev/null +++ b/google-cloud-bigtable/src/test/resources/proto_schema_bundle.pb @@ -0,0 +1,6 @@ + +q +proto_schema_bundle.proto#gcloud.bigtable.schema_bundles.test"' +Author + +first_name ( R firstNamebproto3 \ No newline at end of file diff --git a/google-cloud-bigtable/src/test/resources/proto_schema_bundle.proto b/google-cloud-bigtable/src/test/resources/proto_schema_bundle.proto new file mode 100644 index 000000000000..e03f0ccccbef --- /dev/null +++ b/google-cloud-bigtable/src/test/resources/proto_schema_bundle.proto @@ -0,0 +1,22 @@ +/* +Copyright 2025 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 + http://www.apache.org/licenses/LICENSE-2.0 +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ +// The `proto_schema_bundle.pb` binary is generated from this source file, via command: +// protoc --include_imports --descriptor_set_out=proto_schema_bundle.pb proto_schema_bundle.proto + +syntax = "proto3"; + +package gcloud.bigtable.schema_bundles.test; + +message Author { + string first_name = 1; +} diff --git a/google-cloud-bigtable/src/test/resources/updated_proto_schema_bundle.pb b/google-cloud-bigtable/src/test/resources/updated_proto_schema_bundle.pb new file mode 100644 index 000000000000..21f877a2fe11 --- /dev/null +++ b/google-cloud-bigtable/src/test/resources/updated_proto_schema_bundle.pb @@ -0,0 +1,7 @@ + +– +!updated_proto_schema_bundle.proto#gcloud.bigtable.schema_bundles.test"D +Author + +first_name ( R firstName + last_name ( RlastNamebproto3 \ No newline at end of file diff --git a/google-cloud-bigtable/src/test/resources/updated_proto_schema_bundle.proto b/google-cloud-bigtable/src/test/resources/updated_proto_schema_bundle.proto new file mode 100644 index 000000000000..e9894cabd20a --- /dev/null +++ b/google-cloud-bigtable/src/test/resources/updated_proto_schema_bundle.proto @@ -0,0 +1,23 @@ +/* +Copyright 2025 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 + http://www.apache.org/licenses/LICENSE-2.0 +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ +// The `updated_proto_schema_bundle.pb` binary is generated from this source file, via command: +// protoc --include_imports --descriptor_set_out=updated_proto_schema_bundle.pb updated_proto_schema_bundle.proto + +syntax = "proto3"; + +package gcloud.bigtable.schema_bundles.test; + +message Author { + string first_name = 1; + string last_name = 2; +}