Skip to content

Commit

Permalink
feat: add support for UpdateDatabase in Cloud Spanner (#2265)
Browse files Browse the repository at this point in the history
* feature: Cloud Spanner Drop Database Protection

Co-authored-by: Owl Bot <gcf-owl-bot[bot]@users.noreply.github.com>
Co-authored-by: Rajat Bhatta <93644539+rajatbhatta@users.noreply.github.com>
  • Loading branch information
3 people committed May 15, 2023
1 parent 09f20bd commit 2ea06e7
Show file tree
Hide file tree
Showing 10 changed files with 374 additions and 19 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -358,6 +359,47 @@ OperationFuture<Database, RestoreDatabaseMetadata> 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.
*
* <p>Until completion of the returned operation:
*
* <ul>
* <li>Cancelling the operation is best effort and may or may not succeed.
* <li>All other attempts to modify the database are rejected.
* <li>Reading the database via the API continues to give the pre-request field values.
* </ul>
*
* Upon completion of the returned operation:
*
* <ul>
* <li>The database's new fields are readable via the API.
* </ul>
*
* <p>Example of updating a database.
*
* <pre>{@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<Database, UpdateDatabaseMetadata> op = databaseAdminClient.updateDatabase(
* databaseToUpdate, DatabaseField.DROP_PROTECTION);
* Database updateDatabase = op.get(5, TimeUnit.MINUTES);
* }</pre>
*
* @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<Database, UpdateDatabaseMetadata> updateDatabase(
Database database, DatabaseInfo.DatabaseField... fieldsToUpdate) throws SpannerException;

/**
* Gets the current state of a Cloud Spanner database backup.
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -415,6 +416,27 @@ public Database getDatabase(String instanceId, String databaseId) throws Spanner
return Database.fromProto(rpc.getDatabase(dbName), DatabaseAdminClientImpl.this);
}

@Override
public OperationFuture<Database, UpdateDatabaseMetadata> updateDatabase(
Database database, DatabaseField... fieldsToUpdate) throws SpannerException {
FieldMask fieldMask = DatabaseInfo.DatabaseField.toFieldMask(fieldsToUpdate);
OperationFuture<com.google.spanner.admin.database.v1.Database, UpdateDatabaseMetadata>
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<Void, UpdateDatabaseDdlMetadata> updateDatabaseDdl(
final String instanceId,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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);

Expand Down Expand Up @@ -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. */
Expand All @@ -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) {
Expand Down Expand Up @@ -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;
Expand All @@ -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;
Expand All @@ -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) {
Expand All @@ -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;
}

Expand All @@ -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;
}

Expand Down Expand Up @@ -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;
Expand All @@ -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
Expand All @@ -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,
Expand All @@ -313,6 +412,8 @@ public String toString() {
earliestVersionTime,
encryptionConfig,
defaultLeader,
dialect);
dialect,
dropProtectionEnabled,
reconciling);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
Loading

0 comments on commit 2ea06e7

Please sign in to comment.