diff --git a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/Database.java b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/Database.java index 94c30c9c70..92c4d55d4d 100644 --- a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/Database.java +++ b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/Database.java @@ -194,17 +194,56 @@ private String database() { static Database fromProto( com.google.spanner.admin.database.v1.Database proto, DatabaseAdminClient client) { checkArgument(!proto.getName().isEmpty(), "Missing expected 'name' field"); - return new Database.Builder(client, DatabaseId.of(proto.getName())) - .setState(fromProtoState(proto.getState())) - .setCreateTime(Timestamp.fromProto(proto.getCreateTime())) - .setRestoreInfo(RestoreInfo.fromProtoOrNullIfDefaultInstance(proto.getRestoreInfo())) - .setVersionRetentionPeriod(proto.getVersionRetentionPeriod()) - .setEarliestVersionTime(Timestamp.fromProto(proto.getEarliestVersionTime())) - .setEncryptionConfig(CustomerManagedEncryption.fromProtoOrNull(proto.getEncryptionConfig())) - .setDefaultLeader(proto.getDefaultLeader()) - .setDialect(Dialect.fromProto(proto.getDatabaseDialect())) - .setProto(proto) - .build(); + DatabaseInfo.Builder builder = + new Builder(client, DatabaseId.of(proto.getName())) + .setState(fromProtoState(proto.getState())) + .setCreateTime(Timestamp.fromProto(proto.getCreateTime())) + .setRestoreInfo(RestoreInfo.fromProtoOrNullIfDefaultInstance(proto.getRestoreInfo())) + .setVersionRetentionPeriod(proto.getVersionRetentionPeriod()) + .setEarliestVersionTime(Timestamp.fromProto(proto.getEarliestVersionTime())) + .setEncryptionConfig( + CustomerManagedEncryption.fromProtoOrNull(proto.getEncryptionConfig())) + .setDefaultLeader(proto.getDefaultLeader()) + .setDialect(Dialect.fromProto(proto.getDatabaseDialect())) + .setReconciling(proto.getReconciling()) + .setProto(proto); + if (proto.getEnableDropProtection()) { + builder.enableDropProtection(); + } else { + builder.disableDropProtection(); + } + return builder.build(); + } + + public com.google.spanner.admin.database.v1.Database toProto() { + com.google.spanner.admin.database.v1.Database.Builder builder = + com.google.spanner.admin.database.v1.Database.newBuilder() + .setName(getId().getName()) + .setState(getState().toProto()) + .setEnableDropProtection(isDropProtectionEnabled()) + .setReconciling(getReconciling()); + if (getCreateTime() != null) { + builder.setCreateTime(getCreateTime().toProto()); + } + if (getRestoreInfo() != null) { + builder.setRestoreInfo(getRestoreInfo().getProto()); + } + if (getVersionRetentionPeriod() != null) { + builder.setVersionRetentionPeriod(getVersionRetentionPeriod()); + } + if (getEarliestVersionTime() != null) { + builder.setEarliestVersionTime(getEarliestVersionTime().toProto()); + } + if (getEncryptionConfig() != null) { + builder.setEncryptionConfig(getEncryptionConfig().toProto()); + } + if (getDefaultLeader() != null) { + builder.setDefaultLeader(getDefaultLeader()); + } + if (getDialect() != null) { + builder.setDatabaseDialect(getDialect().toProto()); + } + return builder.build(); } static DatabaseInfo.State fromProtoState( diff --git a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/DatabaseAdminClient.java b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/DatabaseAdminClient.java index 1363118e3a..9168c2a11d 100644 --- a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/DatabaseAdminClient.java +++ b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/DatabaseAdminClient.java @@ -28,6 +28,7 @@ import com.google.spanner.admin.database.v1.CreateDatabaseRequest; import com.google.spanner.admin.database.v1.RestoreDatabaseMetadata; import com.google.spanner.admin.database.v1.UpdateDatabaseDdlMetadata; +import com.google.spanner.admin.database.v1.UpdateDatabaseMetadata; import java.util.List; import javax.annotation.Nullable; @@ -358,6 +359,47 @@ OperationFuture restoreDatabase(Restore resto */ Database getDatabase(String instanceId, String databaseId) throws SpannerException; + /** + * Updates a Cloud Spanner database. The returned {@code Operation} can be used to track the + * progress of the update. Throws SpannerException if the Cloud Spanner database does not exist. + * + *

Until completion of the returned operation: + * + *

+ * + * Upon completion of the returned operation: + * + * + * + *

Example of updating a database. + * + *

{@code
+   * String projectId = my_project_id;
+   * String instanceId = my_instance_id;
+   * String databaseId = my_database_id;
+   * Database databaseToUpdate = databaseAdminClient.newDatabaseBuilder(
+   *         DatabaseId.of(projectId, instanceId, databaseId))
+   *      .enableDropProtection().build();
+   * OperationFuture op = databaseAdminClient.updateDatabase(
+   *           databaseToUpdate, DatabaseField.DROP_PROTECTION);
+   * Database updateDatabase = op.get(5, TimeUnit.MINUTES);
+   * }
+ * + * @param database The database to update to. The current field values of the database will be + * updated to the values specified in this parameter. + * @param fieldsToUpdate The fields that should be updated. Only these fields will have their + * values updated to the values specified in {@param database}, even if there are other fields + * specified in {@param database}. + */ + OperationFuture updateDatabase( + Database database, DatabaseInfo.DatabaseField... fieldsToUpdate) throws SpannerException; + /** * Gets the current state of a Cloud Spanner database backup. * diff --git a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/DatabaseAdminClientImpl.java b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/DatabaseAdminClientImpl.java index 73ece214c3..8a5d0d613a 100644 --- a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/DatabaseAdminClientImpl.java +++ b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/DatabaseAdminClientImpl.java @@ -23,6 +23,7 @@ import com.google.cloud.Policy; import com.google.cloud.Policy.DefaultMarshaller; import com.google.cloud.Timestamp; +import com.google.cloud.spanner.DatabaseInfo.DatabaseField; import com.google.cloud.spanner.Options.ListOption; import com.google.cloud.spanner.SpannerImpl.PageFetcher; import com.google.cloud.spanner.spi.v1.SpannerRpc; @@ -415,6 +416,27 @@ public Database getDatabase(String instanceId, String databaseId) throws Spanner return Database.fromProto(rpc.getDatabase(dbName), DatabaseAdminClientImpl.this); } + @Override + public OperationFuture updateDatabase( + Database database, DatabaseField... fieldsToUpdate) throws SpannerException { + FieldMask fieldMask = DatabaseInfo.DatabaseField.toFieldMask(fieldsToUpdate); + OperationFuture + rawOperationFuture = rpc.updateDatabase(database.toProto(), fieldMask); + return new OperationFutureImpl<>( + rawOperationFuture.getPollingFuture(), + rawOperationFuture.getInitialFuture(), + snapshot -> + Database.fromProto( + ProtoOperationTransformers.ResponseTransformer.create( + com.google.spanner.admin.database.v1.Database.class) + .apply(snapshot), + DatabaseAdminClientImpl.this), + ProtoOperationTransformers.MetadataTransformer.create(UpdateDatabaseMetadata.class), + e -> { + throw SpannerExceptionFactory.newSpannerException(e); + }); + } + @Override public OperationFuture updateDatabaseDdl( final String instanceId, diff --git a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/DatabaseInfo.java b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/DatabaseInfo.java index 565517e341..d231ef34e3 100644 --- a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/DatabaseInfo.java +++ b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/DatabaseInfo.java @@ -16,15 +16,42 @@ package com.google.cloud.spanner; +import com.google.cloud.FieldSelector; import com.google.cloud.Timestamp; import com.google.cloud.spanner.encryption.CustomerManagedEncryption; import com.google.common.base.Preconditions; +import com.google.protobuf.FieldMask; +import com.google.spanner.admin.database.v1.Database.State; import java.util.Objects; import javax.annotation.Nullable; /** Represents a Cloud Spanner database. */ public class DatabaseInfo { + /** Represent an updatable field in a Cloud Spanner database. */ + public enum DatabaseField implements FieldSelector { + DROP_PROTECTION("enable_drop_protection"); + + private final String selector; + + DatabaseField(String selector) { + this.selector = selector; + } + + @Override + public String getSelector() { + return selector; + } + + static FieldMask toFieldMask(DatabaseInfo.DatabaseField... fields) { + FieldMask.Builder builder = FieldMask.newBuilder(); + for (DatabaseInfo.DatabaseField field : fields) { + builder.addPaths(field.getSelector()); + } + return builder.build(); + } + } + public abstract static class Builder { abstract Builder setState(State state); @@ -58,6 +85,18 @@ public Builder setDialect(Dialect dialect) { throw new UnsupportedOperationException("Unimplemented"); } + public Builder enableDropProtection() { + throw new UnsupportedOperationException("Unimplemented"); + } + + public Builder disableDropProtection() { + throw new UnsupportedOperationException("Unimplemented"); + } + + protected Builder setReconciling(boolean reconciling) { + throw new UnsupportedOperationException("Unimplemented"); + } + abstract Builder setProto(com.google.spanner.admin.database.v1.Database proto); /** Builds the database from this builder. */ @@ -74,6 +113,8 @@ abstract static class BuilderImpl extends Builder { private CustomerManagedEncryption encryptionConfig; private String defaultLeader; private Dialect dialect = Dialect.GOOGLE_STANDARD_SQL; + private boolean dropProtectionEnabled; + private boolean reconciling; private com.google.spanner.admin.database.v1.Database proto; BuilderImpl(DatabaseId id) { @@ -141,6 +182,24 @@ public Builder setDialect(Dialect dialect) { return this; } + @Override + public Builder enableDropProtection() { + this.dropProtectionEnabled = true; + return this; + } + + @Override + public Builder disableDropProtection() { + this.dropProtectionEnabled = false; + return this; + } + + @Override + protected Builder setReconciling(boolean reconciling) { + this.reconciling = reconciling; + return this; + } + @Override Builder setProto(@Nullable com.google.spanner.admin.database.v1.Database proto) { this.proto = proto; @@ -151,13 +210,35 @@ Builder setProto(@Nullable com.google.spanner.admin.database.v1.Database proto) /** State of the database. */ public enum State { // Not specified. - UNSPECIFIED, + UNSPECIFIED { + @Override + public com.google.spanner.admin.database.v1.Database.State toProto() { + return com.google.spanner.admin.database.v1.Database.State.STATE_UNSPECIFIED; + } + }, // The database is still being created and is not ready to use. - CREATING, + CREATING { + @Override + public com.google.spanner.admin.database.v1.Database.State toProto() { + return com.google.spanner.admin.database.v1.Database.State.CREATING; + } + }, // The database is fully created and ready to use. - READY, + READY { + @Override + public com.google.spanner.admin.database.v1.Database.State toProto() { + return com.google.spanner.admin.database.v1.Database.State.READY; + } + }, // The database has restored and is being optimized for use. - READY_OPTIMIZING + READY_OPTIMIZING { + @Override + public com.google.spanner.admin.database.v1.Database.State toProto() { + return com.google.spanner.admin.database.v1.Database.State.READY_OPTIMIZING; + } + }; + + public abstract com.google.spanner.admin.database.v1.Database.State toProto(); } private final DatabaseId id; @@ -169,6 +250,8 @@ public enum State { private final CustomerManagedEncryption encryptionConfig; private final String defaultLeader; private final Dialect dialect; + private final boolean dropProtectionEnabled; + private final boolean reconciling; private final com.google.spanner.admin.database.v1.Database proto; public DatabaseInfo(DatabaseId id, State state) { @@ -181,6 +264,8 @@ public DatabaseInfo(DatabaseId id, State state) { this.encryptionConfig = null; this.defaultLeader = null; this.dialect = null; + this.dropProtectionEnabled = false; + this.reconciling = false; this.proto = null; } @@ -194,6 +279,8 @@ public DatabaseInfo(DatabaseId id, State state) { this.encryptionConfig = builder.encryptionConfig; this.defaultLeader = builder.defaultLeader; this.dialect = builder.dialect; + this.dropProtectionEnabled = builder.dropProtectionEnabled; + this.reconciling = builder.reconciling; this.proto = builder.proto; } @@ -262,6 +349,14 @@ public Timestamp getEarliestVersionTime() { return dialect; } + public boolean isDropProtectionEnabled() { + return dropProtectionEnabled; + } + + public boolean getReconciling() { + return reconciling; + } + /** Returns the raw proto instance that was used to construct this {@link Database}. */ public @Nullable com.google.spanner.admin.database.v1.Database getProto() { return proto; @@ -284,7 +379,9 @@ public boolean equals(Object o) { && Objects.equals(earliestVersionTime, that.earliestVersionTime) && Objects.equals(encryptionConfig, that.encryptionConfig) && Objects.equals(defaultLeader, that.defaultLeader) - && Objects.equals(dialect, that.dialect); + && Objects.equals(dialect, that.dialect) + && Objects.equals(dropProtectionEnabled, that.dropProtectionEnabled) + && Objects.equals(reconciling, that.reconciling); } @Override @@ -298,13 +395,15 @@ public int hashCode() { earliestVersionTime, encryptionConfig, defaultLeader, - dialect); + dialect, + dropProtectionEnabled, + reconciling); } @Override public String toString() { return String.format( - "Database[%s, %s, %s, %s, %s, %s, %s, %s, %s]", + "Database[%s, %s, %s, %s, %s, %s, %s, %s, %s %s %s]", id.getName(), state, createTime, @@ -313,6 +412,8 @@ public String toString() { earliestVersionTime, encryptionConfig, defaultLeader, - dialect); + dialect, + dropProtectionEnabled, + reconciling); } } diff --git a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/encryption/CustomerManagedEncryption.java b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/encryption/CustomerManagedEncryption.java index fdc48cf3d2..b24c476729 100644 --- a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/encryption/CustomerManagedEncryption.java +++ b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/encryption/CustomerManagedEncryption.java @@ -42,6 +42,10 @@ public static CustomerManagedEncryption fromProtoOrNull(EncryptionConfig proto) : new CustomerManagedEncryption(proto.getKmsKeyName()); } + public EncryptionConfig toProto() { + return EncryptionConfig.newBuilder().setKmsKeyName(this.getKmsKeyName()).build(); + } + @Override public boolean equals(Object o) { if (this == o) { diff --git a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/spi/v1/GapicSpannerRpc.java b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/spi/v1/GapicSpannerRpc.java index 4d70a26866..5395409782 100644 --- a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/spi/v1/GapicSpannerRpc.java +++ b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/spi/v1/GapicSpannerRpc.java @@ -132,6 +132,8 @@ import com.google.spanner.admin.database.v1.UpdateBackupRequest; import com.google.spanner.admin.database.v1.UpdateDatabaseDdlMetadata; import com.google.spanner.admin.database.v1.UpdateDatabaseDdlRequest; +import com.google.spanner.admin.database.v1.UpdateDatabaseMetadata; +import com.google.spanner.admin.database.v1.UpdateDatabaseRequest; import com.google.spanner.admin.instance.v1.CreateInstanceConfigMetadata; import com.google.spanner.admin.instance.v1.CreateInstanceConfigRequest; import com.google.spanner.admin.instance.v1.CreateInstanceMetadata; @@ -1252,6 +1254,17 @@ public Database getDatabase(String databaseName) throws SpannerException { () -> get(databaseAdminStub.getDatabaseCallable().futureCall(request, context))); } + @Override + public OperationFuture updateDatabase( + Database database, FieldMask updateMask) throws SpannerException { + UpdateDatabaseRequest request = + UpdateDatabaseRequest.newBuilder().setDatabase(database).setUpdateMask(updateMask).build(); + GrpcCallContext context = + newCallContext( + null, database.getName(), request, DatabaseAdminGrpc.getUpdateDatabaseMethod()); + return databaseAdminStub.updateDatabaseOperationCallable().futureCall(request, context); + } + @Override public List getDatabaseDdl(String databaseName) throws SpannerException { acquireAdministrativeRequestsRateLimiter(); diff --git a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/spi/v1/SpannerRpc.java b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/spi/v1/SpannerRpc.java index 552c53e85e..27adf89a23 100644 --- a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/spi/v1/SpannerRpc.java +++ b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/spi/v1/SpannerRpc.java @@ -42,6 +42,7 @@ import com.google.spanner.admin.database.v1.DatabaseRole; import com.google.spanner.admin.database.v1.RestoreDatabaseMetadata; import com.google.spanner.admin.database.v1.UpdateDatabaseDdlMetadata; +import com.google.spanner.admin.database.v1.UpdateDatabaseMetadata; import com.google.spanner.admin.instance.v1.CreateInstanceConfigMetadata; import com.google.spanner.admin.instance.v1.CreateInstanceMetadata; import com.google.spanner.admin.instance.v1.Instance; @@ -232,6 +233,19 @@ OperationFuture updateDatabaseDdl( Database getDatabase(String databaseName) throws SpannerException; + /** + * Updates the specified fields of a Cloud Spanner database. + * + * @param database The database proto whose field values will be used as the new values in the + * stored database. + * @param fieldMask The fields to update. Currently, only the "enable_drop_protection" field of + * the database supports updates. + * @return an `OperationFuture` that can be used to track the status of the update. + * @throws SpannerException + */ + OperationFuture updateDatabase( + Database database, FieldMask fieldMask) throws SpannerException; + List getDatabaseDdl(String databaseName) throws SpannerException; /** Lists the backups in the specified instance. */ Paginated listBackups( diff --git a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/DatabaseAdminClientImplTest.java b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/DatabaseAdminClientImplTest.java index 0255f6c166..dbb08f51a4 100644 --- a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/DatabaseAdminClientImplTest.java +++ b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/DatabaseAdminClientImplTest.java @@ -18,7 +18,9 @@ import static com.google.common.truth.Truth.assertThat; import static com.google.spanner.admin.database.v1.DatabaseDialect.GOOGLE_STANDARD_SQL; +import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertThrows; +import static org.junit.Assert.assertTrue; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; import static org.mockito.MockitoAnnotations.initMocks; @@ -27,6 +29,7 @@ import com.google.cloud.Identity; import com.google.cloud.Role; import com.google.cloud.Timestamp; +import com.google.cloud.spanner.DatabaseInfo.DatabaseField; import com.google.cloud.spanner.DatabaseInfo.State; import com.google.cloud.spanner.encryption.EncryptionConfigs; import com.google.cloud.spanner.spi.v1.SpannerRpc; @@ -53,6 +56,7 @@ import com.google.spanner.admin.database.v1.EncryptionInfo; import com.google.spanner.admin.database.v1.RestoreDatabaseMetadata; import com.google.spanner.admin.database.v1.UpdateDatabaseDdlMetadata; +import com.google.spanner.admin.database.v1.UpdateDatabaseMetadata; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; @@ -246,6 +250,24 @@ public void updateDatabaseDdlOpAlreadyExists() throws Exception { assertThat(op.getName()).isEqualTo(originalOpName); } + @Test + public void updateDatabase() throws Exception { + com.google.cloud.spanner.Database database = + client.newDatabaseBuilder(DatabaseId.of(DB_NAME)).enableDropProtection().build(); + Database databaseProto = database.toProto(); + OperationFuture rawOperationFuture = + OperationFutureUtil.immediateOperationFuture( + "updateDatabase", databaseProto, UpdateDatabaseMetadata.getDefaultInstance()); + when(rpc.updateDatabase( + databaseProto, DatabaseField.toFieldMask(DatabaseField.DROP_PROTECTION))) + .thenReturn(rawOperationFuture); + OperationFuture op = + client.updateDatabase(database, DatabaseField.DROP_PROTECTION); + assertTrue(op.isDone()); + assertEquals(op.get().getId().getName(), DB_NAME); + assertTrue(op.get().isDropProtectionEnabled()); + } + @Test public void dropDatabase() { client.dropDatabase(INSTANCE_ID, DB_ID); diff --git a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/DatabaseTest.java b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/DatabaseTest.java index 1eb2794c8e..78c04ae61b 100644 --- a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/DatabaseTest.java +++ b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/DatabaseTest.java @@ -67,6 +67,10 @@ public class DatabaseTest { private static final String DEFAULT_LEADER = "default-leader"; private static final DatabaseDialect DEFAULT_DIALECT = DatabaseDialect.GOOGLE_STANDARD_SQL; + private static final boolean DROP_PROTECTION_ENABLED = true; + + private static final boolean RECONCILING = true; + @Mock DatabaseAdminClient dbClient; @Before @@ -113,6 +117,22 @@ public void testFromProto() { EncryptionConfigs.customerManagedEncryption(KMS_KEY_NAME), database.getEncryptionConfig()); assertEquals(DEFAULT_LEADER, database.getDefaultLeader()); assertEquals(Dialect.GOOGLE_STANDARD_SQL, database.getDialect()); + assertEquals(DROP_PROTECTION_ENABLED, database.isDropProtectionEnabled()); + assertEquals(RECONCILING, database.getReconciling()); + } + + @Test + public void testToProto() { + final com.google.spanner.admin.database.v1.Database database = createDatabase().toProto(); + assertEquals(NAME, database.getName()); + assertEquals(com.google.spanner.admin.database.v1.Database.State.CREATING, database.getState()); + assertEquals(VERSION_RETENTION_PERIOD, database.getVersionRetentionPeriod()); + assertEquals(EARLIEST_VERSION_TIME.toProto(), database.getEarliestVersionTime()); + assertEquals(ENCRYPTION_CONFIG, database.getEncryptionConfig()); + assertEquals(DEFAULT_LEADER, database.getDefaultLeader()); + assertEquals(DEFAULT_DIALECT, database.getDatabaseDialect()); + assertEquals(DROP_PROTECTION_ENABLED, database.getEnableDropProtection()); + assertEquals(RECONCILING, database.getReconciling()); } @Test @@ -231,6 +251,8 @@ private com.google.spanner.admin.database.v1.Database defaultProtoDatabase() { .addAllEncryptionInfo(ENCRYPTION_INFOS) .setDefaultLeader(DEFAULT_LEADER) .setDatabaseDialect(DEFAULT_DIALECT) + .setEnableDropProtection(DROP_PROTECTION_ENABLED) + .setReconciling(RECONCILING) .build(); } } diff --git a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/it/ITDatabaseAdminTest.java b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/it/ITDatabaseAdminTest.java index e19272a076..6bb02a7a61 100644 --- a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/it/ITDatabaseAdminTest.java +++ b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/it/ITDatabaseAdminTest.java @@ -19,6 +19,8 @@ import static com.google.cloud.spanner.testing.EmulatorSpannerHelper.isUsingEmulator; import static com.google.common.truth.Truth.assertThat; import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertThrows; import static org.junit.Assert.assertTrue; import static org.junit.Assert.fail; import static org.junit.Assume.assumeFalse; @@ -27,6 +29,7 @@ import com.google.api.gax.paging.Page; import com.google.cloud.spanner.Database; import com.google.cloud.spanner.DatabaseAdminClient; +import com.google.cloud.spanner.DatabaseInfo.DatabaseField; import com.google.cloud.spanner.DatabaseRole; import com.google.cloud.spanner.Dialect; import com.google.cloud.spanner.ErrorCode; @@ -39,10 +42,14 @@ import com.google.common.collect.Iterables; import com.google.spanner.admin.database.v1.CreateDatabaseMetadata; import com.google.spanner.admin.database.v1.UpdateDatabaseDdlMetadata; +import com.google.spanner.admin.database.v1.UpdateDatabaseMetadata; import java.util.ArrayList; import java.util.List; import java.util.Optional; +import java.util.concurrent.ExecutionException; import java.util.concurrent.TimeUnit; +import java.util.logging.Level; +import java.util.logging.Logger; import org.junit.After; import org.junit.Before; import org.junit.ClassRule; @@ -57,6 +64,8 @@ public class ITDatabaseAdminTest { private static final long TIMEOUT_MINUTES = 5; @ClassRule public static IntegrationTestEnv env = new IntegrationTestEnv(); + + private static final Logger logger = Logger.getLogger(ITDatabaseAdminTest.class.getName()); private DatabaseAdminClient dbAdminClient; private RemoteSpannerHelper testHelper; private List dbs = new ArrayList<>(); @@ -269,4 +278,71 @@ public void createAndListDatabaseRoles() throws Exception { } assertThat(dbRolesRemaining).containsNoneIn(dbRoles); } + + @Test + public void updateDatabaseInvalidFieldsToUpdate() throws Exception { + assumeFalse("Emulator does not drop database protection", isUsingEmulator()); + String instanceId = testHelper.getInstanceId().getInstance(); + Database database = + dbAdminClient + .createDatabase(instanceId, testHelper.getUniqueDatabaseId(), ImmutableList.of()) + .get(); + logger.log(Level.INFO, "Created database: {0}", database.getId().getName()); + + Database databaseToUpdate = + dbAdminClient.newDatabaseBuilder(database.getId()).enableDropProtection().build(); + // Don't provide any fields to update. + OperationFuture op = + dbAdminClient.updateDatabase(databaseToUpdate); + + ExecutionException e = + assertThrows(ExecutionException.class, () -> op.get(5, TimeUnit.MINUTES)); + assertThat(e.getMessage()).contains("Invalid field mask"); + } + + @Test + public void dropDatabaseWithProtectionEnabled() throws Exception { + assumeFalse("Emulator does not drop database protection", isUsingEmulator()); + String instanceId = testHelper.getInstanceId().getInstance(); + Database database = + dbAdminClient + .createDatabase(instanceId, testHelper.getUniqueDatabaseId(), ImmutableList.of()) + .get(); + logger.log(Level.INFO, "Created database: {0}", database.getId().getName()); + + // Enable drop protection for the database. + Database databaseToUpdate = + dbAdminClient.newDatabaseBuilder(database.getId()).enableDropProtection().build(); + OperationFuture op = + dbAdminClient.updateDatabase(databaseToUpdate, DatabaseField.DROP_PROTECTION); + Database updatedDatabase = op.get(5, TimeUnit.MINUTES); + assertEquals(updatedDatabase.getId().getName(), database.getId().getName()); + assertTrue(updatedDatabase.isDropProtectionEnabled()); + + String databaseId = database.getId().getDatabase(); + + // Assert that dropping a database with protection enabled fails due to precondition violation. + SpannerException e = + assertThrows( + SpannerException.class, () -> dbAdminClient.dropDatabase(instanceId, databaseId)); + assertEquals(ErrorCode.FAILED_PRECONDITION, e.getErrorCode()); + + // Assert that deleting the instance also fails due to precondition violation. + e = + assertThrows( + SpannerException.class, + () -> testHelper.getClient().getInstanceAdminClient().deleteInstance(instanceId)); + assertEquals(ErrorCode.FAILED_PRECONDITION, e.getErrorCode()); + + // Disable drop protection for the database. + databaseToUpdate = + dbAdminClient.newDatabaseBuilder(database.getId()).disableDropProtection().build(); + op = dbAdminClient.updateDatabase(databaseToUpdate, DatabaseField.DROP_PROTECTION); + updatedDatabase = op.get(5, TimeUnit.MINUTES); + assertEquals(updatedDatabase.getId().getName(), database.getId().getName()); + assertFalse(updatedDatabase.isDropProtectionEnabled()); + + // Dropping the database should succeed now. + dbAdminClient.dropDatabase(instanceId, databaseId); + } }