diff --git a/README.md b/README.md
index 00209a3ee31..3cc889c220c 100644
--- a/README.md
+++ b/README.md
@@ -56,13 +56,13 @@ implementation 'com.google.cloud:google-cloud-spanner'
If you are using Gradle without BOM, add this to your dependencies
```Groovy
-implementation 'com.google.cloud:google-cloud-spanner:6.21.2'
+implementation 'com.google.cloud:google-cloud-spanner:6.22.0'
```
If you are using SBT, add this to your dependencies
```Scala
-libraryDependencies += "com.google.cloud" % "google-cloud-spanner" % "6.21.2"
+libraryDependencies += "com.google.cloud" % "google-cloud-spanner" % "6.22.0"
```
## Authentication
diff --git a/google-cloud-spanner/clirr-ignored-differences.xml b/google-cloud-spanner/clirr-ignored-differences.xml
index 464f6e9e9f8..899b44e3546 100644
--- a/google-cloud-spanner/clirr-ignored-differences.xml
+++ b/google-cloud-spanner/clirr-ignored-differences.xml
@@ -35,4 +35,29 @@
com/google/cloud/spanner/connection/ConnectionOptions
com.google.cloud.spanner.Dialect getDialect()
+
+ 7013
+ com/google/cloud/spanner/BackupInfo$Builder
+ com.google.cloud.spanner.BackupInfo$Builder setMaxExpireTime(com.google.cloud.Timestamp)
+
+
+ 7013
+ com/google/cloud/spanner/BackupInfo$Builder
+ com.google.cloud.spanner.BackupInfo$Builder setReferencingBackup(com.google.protobuf.ProtocolStringList)
+
+
+ 7012
+ com/google/cloud/spanner/DatabaseAdminClient
+ com.google.api.gax.longrunning.OperationFuture copyBackup(java.lang.String, java.lang.String, java.lang.String, com.google.cloud.Timestamp)
+
+
+ 7012
+ com/google/cloud/spanner/DatabaseAdminClient
+ com.google.api.gax.longrunning.OperationFuture copyBackup(com.google.cloud.spanner.BackupId, com.google.cloud.spanner.Backup)
+
+
+ 7012
+ com/google/cloud/spanner/spi/v1/SpannerRpc
+ com.google.api.gax.longrunning.OperationFuture copyBackup(com.google.cloud.spanner.BackupId, com.google.cloud.spanner.Backup)
+
diff --git a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/Backup.java b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/Backup.java
index 27308c04009..6d694b0052a 100644
--- a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/Backup.java
+++ b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/Backup.java
@@ -183,6 +183,8 @@ static Backup fromProto(
.setDatabase(DatabaseId.of(proto.getDatabase()))
.setEncryptionInfo(EncryptionInfo.fromProtoOrNull(proto.getEncryptionInfo()))
.setProto(proto)
+ .setMaxExpireTime(Timestamp.fromProto(proto.getMaxExpireTime()))
+ .addAllReferencingBackups(proto.getReferencingBackupsList())
.build();
}
diff --git a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/BackupInfo.java b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/BackupInfo.java
index 0657ff2b7b8..3ea3329a6db 100644
--- a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/BackupInfo.java
+++ b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/BackupInfo.java
@@ -21,6 +21,7 @@
import com.google.cloud.spanner.encryption.BackupEncryptionConfig;
import com.google.cloud.spanner.encryption.EncryptionInfo;
import com.google.spanner.admin.database.v1.Database;
+import java.util.List;
import java.util.Objects;
import javax.annotation.Nullable;
@@ -84,6 +85,24 @@ public abstract static class Builder {
/** Builds the backup from this builder. */
public abstract Backup build();
+
+ /**
+ * Output Only.
+ *
+ *
Returns the max allowed expiration time of the backup, with microseconds granularity.
+ */
+ protected Builder setMaxExpireTime(Timestamp maxExpireTime) {
+ throw new UnsupportedOperationException("Unimplemented");
+ }
+
+ /**
+ * Output Only.
+ *
+ *
Returns the names of the destination backups being created by copying this source backup.
+ */
+ protected Builder addAllReferencingBackups(List referencingBackups) {
+ throw new UnsupportedOperationException("Unimplemented");
+ }
}
abstract static class BuilderImpl extends Builder {
@@ -96,6 +115,8 @@ abstract static class BuilderImpl extends Builder {
private BackupEncryptionConfig encryptionConfig;
private EncryptionInfo encryptionInfo;
private com.google.spanner.admin.database.v1.Backup proto;
+ private Timestamp maxExpireTime;
+ private List referencingBackups;
BuilderImpl(BackupId id) {
this.id = Preconditions.checkNotNull(id);
@@ -111,6 +132,8 @@ abstract static class BuilderImpl extends Builder {
this.encryptionConfig = other.encryptionConfig;
this.encryptionInfo = other.encryptionInfo;
this.proto = other.proto;
+ this.maxExpireTime = other.maxExpireTime;
+ this.referencingBackups = other.referencingBackups;
}
@Override
@@ -163,6 +186,18 @@ Builder setProto(@Nullable com.google.spanner.admin.database.v1.Backup proto) {
this.proto = proto;
return this;
}
+
+ @Override
+ protected Builder setMaxExpireTime(Timestamp maxExpireTime) {
+ this.maxExpireTime = Preconditions.checkNotNull(maxExpireTime);
+ return this;
+ }
+
+ @Override
+ protected Builder addAllReferencingBackups(List referencingBackups) {
+ this.referencingBackups = Preconditions.checkNotNull(referencingBackups);
+ return this;
+ }
}
/** State of the backup. */
@@ -184,6 +219,8 @@ public enum State {
private final BackupEncryptionConfig encryptionConfig;
private final EncryptionInfo encryptionInfo;
private final com.google.spanner.admin.database.v1.Backup proto;
+ private final Timestamp maxExpireTime;
+ private final List referencingBackups;
BackupInfo(BuilderImpl builder) {
this.id = builder.id;
@@ -195,6 +232,8 @@ public enum State {
this.versionTime = builder.versionTime;
this.database = builder.database;
this.proto = builder.proto;
+ this.maxExpireTime = builder.maxExpireTime;
+ this.referencingBackups = builder.referencingBackups;
}
/** Returns the backup id. */
@@ -253,6 +292,19 @@ public DatabaseId getDatabase() {
return proto;
}
+ /** Returns the max expire time of this {@link Backup}. */
+ public Timestamp getMaxExpireTime() {
+ return maxExpireTime;
+ }
+
+ /**
+ * Returns the names of the destination backups being created by copying this source backup {@link
+ * Backup}.
+ */
+ public List getReferencingBackups() {
+ return referencingBackups;
+ }
+
@Override
public boolean equals(Object o) {
if (this == o) {
@@ -269,19 +321,30 @@ public boolean equals(Object o) {
&& Objects.equals(encryptionInfo, that.encryptionInfo)
&& Objects.equals(expireTime, that.expireTime)
&& Objects.equals(versionTime, that.versionTime)
- && Objects.equals(database, that.database);
+ && Objects.equals(database, that.database)
+ && Objects.equals(maxExpireTime, that.maxExpireTime)
+ && Objects.equals(referencingBackups, that.referencingBackups);
}
@Override
public int hashCode() {
return Objects.hash(
- id, state, size, encryptionConfig, encryptionInfo, expireTime, versionTime, database);
+ id,
+ state,
+ size,
+ encryptionConfig,
+ encryptionInfo,
+ expireTime,
+ versionTime,
+ database,
+ maxExpireTime,
+ referencingBackups);
}
@Override
public String toString() {
return String.format(
- "Backup[%s, %s, %d, %s, %s, %s, %s, %s]",
+ "Backup[%s, %s, %d, %s, %s, %s, %s, %s, %s, %s]",
id.getName(),
state,
size,
@@ -289,6 +352,8 @@ public String toString() {
encryptionInfo,
expireTime,
versionTime,
- database);
+ database,
+ maxExpireTime,
+ referencingBackups);
}
}
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 2e4fd09951e..d3aa8b4d18f 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
@@ -22,6 +22,7 @@
import com.google.cloud.Timestamp;
import com.google.cloud.spanner.Options.ListOption;
import com.google.longrunning.Operation;
+import com.google.spanner.admin.database.v1.CopyBackupMetadata;
import com.google.spanner.admin.database.v1.CreateBackupMetadata;
import com.google.spanner.admin.database.v1.CreateDatabaseMetadata;
import com.google.spanner.admin.database.v1.CreateDatabaseRequest;
@@ -178,6 +179,73 @@ OperationFuture createBackup(
*/
OperationFuture createBackup(Backup backup) throws SpannerException;
+ /**
+ * Creates a copy of backup from an existing backup in a Cloud Spanner instance.
+ *
+ * Example to copy a backup.
+ *
+ *
{@code
+ * String instanceId ="my_instance_id";
+ * String sourceBackupId ="source_backup_id";
+ * String destinationBackupId ="destination_backup_id";
+ * Timestamp expireTime =Timestamp.ofTimeMicroseconds(micros);
+ * OperationFuture op = dbAdminClient
+ * .copyBackup(
+ * instanceId,
+ * sourceBackupId,
+ * destinationBackupId,
+ * expireTime);
+ * Backup backup = op.get();
+ * }
+ *
+ * @param instanceId the id of the instance where the source backup is located and where the new
+ * backup will be created.
+ * @param sourceBackupId the source backup id.
+ * @param destinationBackupId the id of the backup which will be created. It must conform to the
+ * regular expression [a-z][a-z0-9_\-]*[a-z0-9] and be between 2 and 60 characters in length.
+ * @param expireTime the time that the new backup will automatically expire.
+ */
+ default OperationFuture copyBackup(
+ String instanceId, String sourceBackupId, String destinationBackupId, Timestamp expireTime) {
+ throw new UnsupportedOperationException("Unimplemented");
+ }
+
+ /**
+ * Creates a copy of backup from an existing backup in Cloud Spanner in the same instance. Any
+ * configuration options in the {@link Backup} instance will be included in the {@link
+ * com.google.spanner.admin.database.v1.CopyBackupRequest}.
+ *
+ * The expire time of the new backup must be set and be at least 6 hours and at most 366 days
+ * after the creation time of the existing backup that is being copied.
+ *
+ *
Example to create a copy of a backup.
+ *
+ *
{@code
+ * BackupId sourceBackupId = BackupId.of("source-project", "source-instance", "source-backup-id");
+ * BackupId destinationBackupId = BackupId.of("destination-project", "destination-instance", "new-backup-id");
+ * Timestamp expireTime = Timestamp.ofTimeMicroseconds(expireTimeMicros);
+ * EncryptionConfig encryptionConfig =
+ * EncryptionConfig.ofKey(
+ * "projects/my-project/locations/some-location/keyRings/my-keyring/cryptoKeys/my-key"));
+ *
+ * Backup destinationBackup = dbAdminClient
+ * .newBackupBuilder(destinationBackupId)
+ * .setExpireTime(expireTime)
+ * .setEncryptionConfig(encryptionConfig)
+ * .build();
+ *
+ * OperationFuture op = dbAdminClient.copyBackup(sourceBackupId, destinationBackup);
+ * Backup copiedBackup = op.get();
+ * }
+ *
+ * @param sourceBackupId the backup to be copied
+ * @param destinationBackup the new backup to create
+ */
+ default OperationFuture copyBackup(
+ BackupId sourceBackupId, Backup destinationBackup) {
+ throw new UnsupportedOperationException("Unimplemented");
+ }
+
/**
* Restore a database from a backup. The database that is restored will be created and may not
* already exist.
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 52df8701556..3285dde5f5e 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
@@ -32,10 +32,7 @@
import com.google.longrunning.Operation;
import com.google.protobuf.Empty;
import com.google.protobuf.FieldMask;
-import com.google.spanner.admin.database.v1.CreateBackupMetadata;
-import com.google.spanner.admin.database.v1.CreateDatabaseMetadata;
-import com.google.spanner.admin.database.v1.RestoreDatabaseMetadata;
-import com.google.spanner.admin.database.v1.UpdateDatabaseDdlMetadata;
+import com.google.spanner.admin.database.v1.*;
import java.util.List;
import java.util.UUID;
import javax.annotation.Nullable;
@@ -165,6 +162,49 @@ public OperationFuture createBackup(Backup backupI
});
}
+ @Override
+ public OperationFuture copyBackup(
+ String instanceId, String sourceBackupId, String destinationBackupId, Timestamp expireTime)
+ throws SpannerException {
+ final Backup destinationBackup =
+ newBackupBuilder(BackupId.of(projectId, instanceId, destinationBackupId))
+ .setExpireTime(expireTime)
+ .build();
+
+ return copyBackup(BackupId.of(projectId, instanceId, sourceBackupId), destinationBackup);
+ }
+
+ @Override
+ public OperationFuture copyBackup(
+ BackupId sourceBackupId, Backup destinationBackup) throws SpannerException {
+ Preconditions.checkNotNull(sourceBackupId);
+ Preconditions.checkNotNull(destinationBackup);
+
+ final OperationFuture
+ rawOperationFuture = rpc.copyBackup(sourceBackupId, destinationBackup);
+
+ return new OperationFutureImpl<>(
+ rawOperationFuture.getPollingFuture(),
+ rawOperationFuture.getInitialFuture(),
+ snapshot -> {
+ com.google.spanner.admin.database.v1.Backup proto =
+ ProtoOperationTransformers.ResponseTransformer.create(
+ com.google.spanner.admin.database.v1.Backup.class)
+ .apply(snapshot);
+ return Backup.fromProto(
+ com.google.spanner.admin.database.v1.Backup.newBuilder(proto)
+ .setName(proto.getName())
+ .setExpireTime(proto.getExpireTime())
+ .setEncryptionInfo(proto.getEncryptionInfo())
+ .build(),
+ DatabaseAdminClientImpl.this);
+ },
+ ProtoOperationTransformers.MetadataTransformer.create(CopyBackupMetadata.class),
+ e -> {
+ throw SpannerExceptionFactory.newSpannerException(e);
+ });
+ }
+
@Override
public Backup updateBackup(String instanceId, String backupId, Timestamp expireTime) {
String backupName = getBackupName(instanceId, backupId);
diff --git a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/encryption/EncryptionConfigProtoMapper.java b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/encryption/EncryptionConfigProtoMapper.java
index 0a18e9844a1..62d51bf76ed 100644
--- a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/encryption/EncryptionConfigProtoMapper.java
+++ b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/encryption/EncryptionConfigProtoMapper.java
@@ -16,6 +16,7 @@
package com.google.cloud.spanner.encryption;
+import com.google.spanner.admin.database.v1.CopyBackupEncryptionConfig;
import com.google.spanner.admin.database.v1.CreateBackupEncryptionConfig;
import com.google.spanner.admin.database.v1.EncryptionConfig;
import com.google.spanner.admin.database.v1.RestoreDatabaseEncryptionConfig;
@@ -50,6 +51,28 @@ public static CreateBackupEncryptionConfig createBackupEncryptionConfig(
}
}
+ /** Returns an encryption config to be used for a copy backup. */
+ public static CopyBackupEncryptionConfig copyBackupEncryptionConfig(
+ BackupEncryptionConfig config) {
+ if (config instanceof CustomerManagedEncryption) {
+ return CopyBackupEncryptionConfig.newBuilder()
+ .setEncryptionType(CopyBackupEncryptionConfig.EncryptionType.CUSTOMER_MANAGED_ENCRYPTION)
+ .setKmsKeyName(((CustomerManagedEncryption) config).getKmsKeyName())
+ .build();
+ } else if (config instanceof GoogleDefaultEncryption) {
+ return CopyBackupEncryptionConfig.newBuilder()
+ .setEncryptionType(CopyBackupEncryptionConfig.EncryptionType.GOOGLE_DEFAULT_ENCRYPTION)
+ .build();
+ } else if (config instanceof UseBackupEncryption) {
+ return CopyBackupEncryptionConfig.newBuilder()
+ .setEncryptionType(
+ CopyBackupEncryptionConfig.EncryptionType.USE_CONFIG_DEFAULT_OR_BACKUP_ENCRYPTION)
+ .build();
+ } else {
+ throw new IllegalArgumentException("Unknown backup encryption configuration " + config);
+ }
+ }
+
/** Returns an encryption config to be used for a database restore. */
public static RestoreDatabaseEncryptionConfig restoreDatabaseEncryptionConfig(
RestoreEncryptionConfig config) {
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 98a7e0927f3..3a20c2a3c78 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
@@ -60,6 +60,7 @@
import com.google.cloud.grpc.GcpManagedChannelOptions.GcpMetricsOptions;
import com.google.cloud.grpc.GrpcTransportOptions;
import com.google.cloud.spanner.AdminRequestsPerMinuteExceededException;
+import com.google.cloud.spanner.BackupId;
import com.google.cloud.spanner.ErrorCode;
import com.google.cloud.spanner.Restore;
import com.google.cloud.spanner.SpannerException;
@@ -102,6 +103,8 @@
import com.google.protobuf.Message;
import com.google.protobuf.Timestamp;
import com.google.spanner.admin.database.v1.Backup;
+import com.google.spanner.admin.database.v1.CopyBackupMetadata;
+import com.google.spanner.admin.database.v1.CopyBackupRequest;
import com.google.spanner.admin.database.v1.CreateBackupMetadata;
import com.google.spanner.admin.database.v1.CreateBackupRequest;
import com.google.spanner.admin.database.v1.CreateDatabaseMetadata;
@@ -1264,6 +1267,65 @@ public OperationFuture createBackup(
NanoClock.getDefaultClock());
}
+ @Override
+ public OperationFuture copyBackup(
+ BackupId sourceBackupId, final com.google.cloud.spanner.Backup destinationBackup)
+ throws SpannerException {
+ Preconditions.checkNotNull(sourceBackupId);
+ Preconditions.checkNotNull(destinationBackup);
+ final String instanceName = destinationBackup.getInstanceId().getName();
+ final String backupId = destinationBackup.getId().getBackup();
+
+ final CopyBackupRequest.Builder requestBuilder =
+ CopyBackupRequest.newBuilder()
+ .setParent(instanceName)
+ .setBackupId(backupId)
+ .setSourceBackup(sourceBackupId.getName())
+ .setExpireTime(destinationBackup.getExpireTime().toProto());
+
+ if (destinationBackup.getEncryptionConfig() != null) {
+ requestBuilder.setEncryptionConfig(
+ EncryptionConfigProtoMapper.copyBackupEncryptionConfig(
+ destinationBackup.getEncryptionConfig()));
+ }
+ final CopyBackupRequest request = requestBuilder.build();
+ final OperationFutureCallable callable =
+ new OperationFutureCallable<>(
+ databaseAdminStub.copyBackupOperationCallable(),
+ request,
+ // calling copy backup method of dbClientImpl
+ DatabaseAdminGrpc.getCopyBackupMethod(),
+ instanceName,
+ nextPageToken ->
+ listBackupOperations(
+ instanceName,
+ 0,
+ String.format(
+ "(metadata.@type:type.googleapis.com/%s) AND (metadata.name:%s)",
+ CopyBackupMetadata.getDescriptor().getFullName(),
+ String.format("%s/backups/%s", instanceName, backupId)),
+ nextPageToken),
+ input -> {
+ try {
+ return input
+ .getMetadata()
+ .unpack(CopyBackupMetadata.class)
+ .getProgress()
+ .getStartTime();
+ } catch (InvalidProtocolBufferException e) {
+ return null;
+ }
+ });
+ return RetryHelper.runWithRetries(
+ callable,
+ databaseAdminStubSettings
+ .copyBackupOperationSettings()
+ .getInitialCallSettings()
+ .getRetrySettings(),
+ new OperationFutureRetryAlgorithm<>(),
+ NanoClock.getDefaultClock());
+ }
+
@Override
public OperationFuture restoreDatabase(final Restore restore) {
final String databaseInstanceName = restore.getDestination().getInstanceId().getName();
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 cce92e0f378..00382e228f3 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
@@ -22,6 +22,7 @@
import com.google.api.gax.retrying.RetrySettings;
import com.google.api.gax.rpc.ServerStream;
import com.google.cloud.ServiceRpc;
+import com.google.cloud.spanner.BackupId;
import com.google.cloud.spanner.Restore;
import com.google.cloud.spanner.SpannerException;
import com.google.cloud.spanner.admin.database.v1.stub.DatabaseAdminStub;
@@ -32,31 +33,12 @@
import com.google.longrunning.Operation;
import com.google.protobuf.Empty;
import com.google.protobuf.FieldMask;
-import com.google.spanner.admin.database.v1.Backup;
-import com.google.spanner.admin.database.v1.CreateBackupMetadata;
-import com.google.spanner.admin.database.v1.CreateDatabaseMetadata;
-import com.google.spanner.admin.database.v1.Database;
-import com.google.spanner.admin.database.v1.RestoreDatabaseMetadata;
-import com.google.spanner.admin.database.v1.UpdateDatabaseDdlMetadata;
+import com.google.spanner.admin.database.v1.*;
import com.google.spanner.admin.instance.v1.CreateInstanceMetadata;
import com.google.spanner.admin.instance.v1.Instance;
import com.google.spanner.admin.instance.v1.InstanceConfig;
import com.google.spanner.admin.instance.v1.UpdateInstanceMetadata;
-import com.google.spanner.v1.BeginTransactionRequest;
-import com.google.spanner.v1.CommitRequest;
-import com.google.spanner.v1.CommitResponse;
-import com.google.spanner.v1.ExecuteBatchDmlRequest;
-import com.google.spanner.v1.ExecuteBatchDmlResponse;
-import com.google.spanner.v1.ExecuteSqlRequest;
-import com.google.spanner.v1.PartialResultSet;
-import com.google.spanner.v1.PartitionQueryRequest;
-import com.google.spanner.v1.PartitionReadRequest;
-import com.google.spanner.v1.PartitionResponse;
-import com.google.spanner.v1.ReadRequest;
-import com.google.spanner.v1.ResultSet;
-import com.google.spanner.v1.RollbackRequest;
-import com.google.spanner.v1.Session;
-import com.google.spanner.v1.Transaction;
+import com.google.spanner.v1.*;
import java.util.List;
import java.util.Map;
import javax.annotation.Nullable;
@@ -230,6 +212,20 @@ Paginated listBackups(
OperationFuture createBackup(
com.google.cloud.spanner.Backup backupInfo) throws SpannerException;
+ /**
+ * Creates a copy backup from the source backup specified.
+ *
+ * @param destinationBackup the backup to create. The instance, database, and expireTime fields of
+ * the backup must be filled. It may also optionally have an encryption config set. If no
+ * encryption config has been set, the new backup will use the same encryption config as the
+ * source backup.
+ * @return the operation that monitors the backup creation.
+ */
+ default OperationFuture copyBackup(
+ BackupId sourceBackupId, com.google.cloud.spanner.Backup destinationBackup) {
+ throw new UnsupportedOperationException("Unimplemented");
+ }
+
/**
* Restore a backup into the given database.
*
diff --git a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/BackupTest.java b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/BackupTest.java
index 2f12790437c..ea1d3724c41 100644
--- a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/BackupTest.java
+++ b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/BackupTest.java
@@ -17,10 +17,7 @@
package com.google.cloud.spanner;
import static com.google.common.truth.Truth.assertThat;
-import static org.junit.Assert.assertEquals;
-import static org.junit.Assert.assertNotNull;
-import static org.junit.Assert.assertNull;
-import static org.junit.Assert.assertThrows;
+import static org.junit.Assert.*;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import static org.mockito.MockitoAnnotations.initMocks;
@@ -47,6 +44,8 @@ public class BackupTest {
private static final String NAME =
"projects/test-project/instances/test-instance/backups/backup-1";
+ private static final String REFERENCING_BACKUP_NAME =
+ "projects/test-project/instances/test-instance/backups/backup-2";
private static final String DB = "projects/test-project/instances/test-instance/databases/db-1";
private static final Timestamp EXP_TIME = Timestamp.ofTimeSecondsAndNanos(1000L, 1000);
private static final Timestamp VERSION_TIME = Timestamp.ofTimeSecondsAndNanos(2000L, 2000);
@@ -293,9 +292,11 @@ public void fromProto() {
public void testEqualsAndHashCode() {
final Backup backup1 = createBackup();
final Backup backup2 = createBackup();
+ final Backup copyBackup1 = copyBackup();
assertEquals(backup1, backup2);
assertEquals(backup1.hashCode(), backup2.hashCode());
+ assertEquals(backup1.hashCode(), copyBackup1.hashCode());
}
private Backup createBackup() {
@@ -309,6 +310,27 @@ private Backup createBackup() {
com.google.protobuf.Timestamp.newBuilder().setSeconds(2000L).setNanos(2000).build())
.setEncryptionInfo(ENCRYPTION_INFO)
.setState(com.google.spanner.admin.database.v1.Backup.State.CREATING)
+ .setMaxExpireTime(
+ com.google.protobuf.Timestamp.newBuilder().setSeconds(3000L).setNanos(3000).build())
+ .addAllReferencingBackups(Collections.singletonList(REFERENCING_BACKUP_NAME))
+ .build();
+ return Backup.fromProto(proto, dbClient);
+ }
+
+ private Backup copyBackup() {
+ com.google.spanner.admin.database.v1.Backup proto =
+ com.google.spanner.admin.database.v1.Backup.newBuilder()
+ .setName(NAME)
+ .setDatabase(DB)
+ .setExpireTime(
+ com.google.protobuf.Timestamp.newBuilder().setSeconds(1000L).setNanos(1000).build())
+ .setVersionTime(
+ com.google.protobuf.Timestamp.newBuilder().setSeconds(2000L).setNanos(2000).build())
+ .setEncryptionInfo(ENCRYPTION_INFO)
+ .setState(com.google.spanner.admin.database.v1.Backup.State.CREATING)
+ .setMaxExpireTime(
+ com.google.protobuf.Timestamp.newBuilder().setSeconds(3000L).setNanos(3000).build())
+ .addAllReferencingBackups(Collections.singletonList(REFERENCING_BACKUP_NAME))
.build();
return Backup.fromProto(proto, dbClient);
}
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 44a531d4a34..56dfd707307 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
@@ -44,6 +44,7 @@
import com.google.protobuf.FieldMask;
import com.google.protobuf.Message;
import com.google.spanner.admin.database.v1.Backup;
+import com.google.spanner.admin.database.v1.CopyBackupMetadata;
import com.google.spanner.admin.database.v1.CreateBackupMetadata;
import com.google.spanner.admin.database.v1.CreateDatabaseMetadata;
import com.google.spanner.admin.database.v1.Database;
@@ -72,6 +73,7 @@ public class DatabaseAdminClientImplTest {
private static final String DB_NAME2 =
"projects/my-project/instances/my-instance/databases/my-db2";
private static final String BK_ID = "my-bk";
+ private static final String SOURCE_BK = "my-source-bk";
private static final String BK_NAME = "projects/my-project/instances/my-instance/backups/my-bk";
private static final String BK_NAME2 = "projects/my-project/instances/my-instance/backups/my-bk2";
private static final Timestamp EARLIEST_VERSION_TIME = Timestamp.now();
@@ -454,6 +456,81 @@ public void createEncryptedBackup() throws ExecutionException, InterruptedExcept
assertThat(op.get().getEncryptionInfo().getKmsKeyVersion()).isEqualTo(KMS_KEY_VERSION);
}
+ @Test
+ public void copyBackupWithParams() throws Exception {
+ OperationFuture rawOperationFuture =
+ OperationFutureUtil.immediateOperationFuture(
+ "copyBackup", getBackupProto(), CopyBackupMetadata.getDefaultInstance());
+ Timestamp t =
+ Timestamp.ofTimeMicroseconds(
+ TimeUnit.MILLISECONDS.toMicros(System.currentTimeMillis())
+ + TimeUnit.HOURS.toMicros(28));
+ final com.google.cloud.spanner.Backup backup =
+ client
+ .newBackupBuilder(BackupId.of(PROJECT_ID, INSTANCE_ID, BK_ID))
+ .setExpireTime(t)
+ .build();
+ when(rpc.copyBackup(BackupId.of(PROJECT_ID, INSTANCE_ID, SOURCE_BK), backup))
+ .thenReturn(rawOperationFuture);
+ OperationFuture op =
+ client.copyBackup(INSTANCE_ID, SOURCE_BK, BK_ID, t);
+ assertThat(op.isDone()).isTrue();
+ assertThat(op.get().getId().getName()).isEqualTo(BK_NAME);
+ }
+
+ @Test
+ public void copyBackupWithBackupObject() throws ExecutionException, InterruptedException {
+ final OperationFuture rawOperationFuture =
+ OperationFutureUtil.immediateOperationFuture(
+ "copyBackup", getBackupProto(), CopyBackupMetadata.getDefaultInstance());
+ final Timestamp expireTime =
+ Timestamp.ofTimeMicroseconds(
+ TimeUnit.MILLISECONDS.toMicros(System.currentTimeMillis())
+ + TimeUnit.HOURS.toMicros(28));
+ final Timestamp versionTime =
+ Timestamp.ofTimeMicroseconds(
+ TimeUnit.MILLISECONDS.toMicros(System.currentTimeMillis()) - TimeUnit.DAYS.toMicros(2));
+ final com.google.cloud.spanner.Backup requestBackup =
+ client
+ .newBackupBuilder(BackupId.of(PROJECT_ID, INSTANCE_ID, BK_ID))
+ .setExpireTime(expireTime)
+ .setVersionTime(versionTime)
+ .build();
+ BackupId sourceBackupId = BackupId.of(PROJECT_ID, INSTANCE_ID, BK_ID);
+
+ when(rpc.copyBackup(sourceBackupId, requestBackup)).thenReturn(rawOperationFuture);
+
+ final OperationFuture op =
+ client.copyBackup(sourceBackupId, requestBackup);
+ assertThat(op.isDone()).isTrue();
+ assertThat(op.get().getId().getName()).isEqualTo(BK_NAME);
+ }
+
+ @Test
+ public void copyEncryptedBackup() throws ExecutionException, InterruptedException {
+ final OperationFuture rawOperationFuture =
+ OperationFutureUtil.immediateOperationFuture(
+ "copyBackup", getEncryptedBackupProto(), CopyBackupMetadata.getDefaultInstance());
+ final Timestamp t =
+ Timestamp.ofTimeMicroseconds(
+ TimeUnit.MILLISECONDS.toMicros(System.currentTimeMillis())
+ + TimeUnit.HOURS.toMicros(28));
+ final com.google.cloud.spanner.Backup backup =
+ client
+ .newBackupBuilder(BackupId.of(PROJECT_ID, INSTANCE_ID, BK_ID))
+ .setDatabase(DatabaseId.of(PROJECT_ID, INSTANCE_ID, DB_ID))
+ .setExpireTime(t)
+ .setEncryptionConfig(EncryptionConfigs.customerManagedEncryption(KMS_KEY_NAME))
+ .build();
+ BackupId sourceBackupId = BackupId.of(PROJECT_ID, INSTANCE_ID, BK_ID);
+ when(rpc.copyBackup(sourceBackupId, backup)).thenReturn(rawOperationFuture);
+ final OperationFuture op =
+ client.copyBackup(sourceBackupId, backup);
+ assertThat(op.isDone()).isTrue();
+ assertThat(op.get().getId().getName()).isEqualTo(BK_NAME);
+ assertThat(op.get().getEncryptionInfo().getKmsKeyVersion()).isEqualTo(KMS_KEY_VERSION);
+ }
+
@Test
public void deleteBackup() {
client.deleteBackup(INSTANCE_ID, BK_ID);