create() {
getExpireTime() != null, "Cannot create a backup without an expire time");
Preconditions.checkState(
getDatabase() != null, "Cannot create a backup without a source database");
- return dbClient.createBackup(instance(), backup(), sourceDatabase(), getExpireTime());
+ return dbClient.createBackup(this);
}
/**
@@ -182,6 +182,7 @@ static Backup fromProto(
.setState(fromProtoState(proto.getState()))
.setSize(proto.getSizeBytes())
.setExpireTime(Timestamp.fromProto(proto.getExpireTime()))
+ .setVersionTime(Timestamp.fromProto(proto.getVersionTime()))
.setDatabase(DatabaseId.of(proto.getDatabase()))
.setProto(proto)
.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 5968bcdc21..199e6ae2ae 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
@@ -18,6 +18,7 @@
import com.google.api.client.util.Preconditions;
import com.google.cloud.Timestamp;
+import com.google.spanner.admin.database.v1.Database;
import java.util.Objects;
import javax.annotation.Nullable;
@@ -40,6 +41,17 @@ public abstract static class Builder {
*/
public abstract Builder setExpireTime(Timestamp expireTime);
+ /**
+ * Optional for creating a new backup.
+ *
+ * Specifies the timestamp to have an externally consistent copy of the database. If no
+ * version time is specified, it will be automatically set to the backup create time.
+ *
+ *
The version time can be as far in the past as specified by the database earliest version
+ * time (see {@link Database#getEarliestVersionTime()}).
+ */
+ public abstract Builder setVersionTime(Timestamp versionTime);
+
/**
* Required for creating a new backup.
*
@@ -55,6 +67,7 @@ abstract static class BuilderImpl extends Builder {
protected final BackupId id;
private State state = State.UNSPECIFIED;
private Timestamp expireTime;
+ private Timestamp versionTime;
private DatabaseId database;
private long size;
private com.google.spanner.admin.database.v1.Backup proto;
@@ -67,6 +80,7 @@ abstract static class BuilderImpl extends Builder {
this.id = other.id;
this.state = other.state;
this.expireTime = other.expireTime;
+ this.versionTime = other.versionTime;
this.database = other.database;
this.size = other.size;
this.proto = other.proto;
@@ -84,6 +98,12 @@ public Builder setExpireTime(Timestamp expireTime) {
return this;
}
+ @Override
+ public Builder setVersionTime(Timestamp versionTime) {
+ this.versionTime = versionTime;
+ return this;
+ }
+
@Override
public Builder setDatabase(DatabaseId database) {
Preconditions.checkArgument(
@@ -119,6 +139,7 @@ public enum State {
private final BackupId id;
private final State state;
private final Timestamp expireTime;
+ private final Timestamp versionTime;
private final DatabaseId database;
private final long size;
private final com.google.spanner.admin.database.v1.Backup proto;
@@ -128,6 +149,7 @@ public enum State {
this.state = builder.state;
this.size = builder.size;
this.expireTime = builder.expireTime;
+ this.versionTime = builder.versionTime;
this.database = builder.database;
this.proto = builder.proto;
}
@@ -157,6 +179,11 @@ public Timestamp getExpireTime() {
return expireTime;
}
+ /** Returns the version time of the backup. */
+ public Timestamp getVersionTime() {
+ return versionTime;
+ }
+
/** Returns the id of the database that was used to create the backup. */
public DatabaseId getDatabase() {
return database;
@@ -180,17 +207,19 @@ public boolean equals(Object o) {
&& state == that.state
&& size == that.size
&& Objects.equals(expireTime, that.expireTime)
+ && Objects.equals(versionTime, that.versionTime)
&& Objects.equals(database, that.database);
}
@Override
public int hashCode() {
- return Objects.hash(id, state, size, expireTime, database);
+ return Objects.hash(id, state, size, expireTime, versionTime, database);
}
@Override
public String toString() {
return String.format(
- "Backup[%s, %s, %d, %s, %s]", id.getName(), state, size, expireTime, database);
+ "Backup[%s, %s, %d, %s, %s, %s]",
+ id.getName(), state, size, expireTime, versionTime, database);
}
}
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 89ee6e875a..a442ad2399 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
@@ -118,8 +118,14 @@ public OperationFuture backup(Backup backup) {
Preconditions.checkArgument(
backup.getInstanceId().equals(getId().getInstanceId()),
"The instance of the backup must be equal to the instance of this database.");
+
return dbClient.createBackup(
- instance(), backup.getId().getBackup(), database(), backup.getExpireTime());
+ dbClient
+ .newBackupBuilder(backup.getId())
+ .setDatabase(getId())
+ .setExpireTime(backup.getExpireTime())
+ .setVersionTime(backup.getVersionTime())
+ .build());
}
/**
@@ -177,6 +183,8 @@ static Database fromProto(
.setState(fromProtoState(proto.getState()))
.setCreateTime(Timestamp.fromProto(proto.getCreateTime()))
.setRestoreInfo(RestoreInfo.fromProtoOrNullIfDefaultInstance(proto.getRestoreInfo()))
+ .setVersionRetentionPeriod(proto.getVersionRetentionPeriod())
+ .setEarliestVersionTime(Timestamp.fromProto(proto.getEarliestVersionTime()))
.setProto(proto)
.build();
}
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 e7a8ed6787..eae1a3cdf4 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
@@ -101,6 +101,32 @@ OperationFuture createBackup(
String sourceInstanceId, String backupId, String databaseId, Timestamp expireTime)
throws SpannerException;
+ /**
+ * Creates a new backup from a database in a Cloud Spanner instance.
+ *
+ * Example to create a backup.
+ *
+ *
{@code
+ * BackupId backupId = BackupId.of("project", "instance", "backup-id");
+ * DatabaseId databaseId = DatabaseId.of("project", "instance", "database-id");
+ * Timestamp expireTime = Timestamp.ofTimeMicroseconds(expireTimeMicros);
+ * Timestamp versionTime = Timestamp.ofTimeMicroseconds(versionTimeMicros);
+ *
+ * Backup backupToCreate = dbAdminClient
+ * .newBackupBuilder(backupId)
+ * .setDatabase(databaseId)
+ * .setExpireTime(expireTime)
+ * .setVersionTime(versionTime)
+ * .build();
+ *
+ * OperationFuture op = dbAdminClient.createBackup(backupToCreate);
+ * Backup createdBackup = op.get();
+ * }
+ *
+ * @param backup the backup to be created
+ */
+ OperationFuture createBackup(Backup backup) throws SpannerException;
+
/**
* 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 28de150cae..a5eed214a4 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
@@ -114,16 +114,31 @@ public Database apply(Exception e) {
public OperationFuture createBackup(
String instanceId, String backupId, String databaseId, Timestamp expireTime)
throws SpannerException {
- com.google.spanner.admin.database.v1.Backup backup =
+ final Backup backup =
+ newBackupBuilder(BackupId.of(projectId, instanceId, backupId))
+ .setDatabase(DatabaseId.of(projectId, instanceId, databaseId))
+ .setExpireTime(expireTime)
+ .build();
+ return createBackup(backup);
+ }
+
+ @Override
+ public OperationFuture createBackup(final Backup backup) {
+ final String instanceId = backup.getInstanceId().getInstance();
+ final String databaseId = backup.getDatabase().getDatabase();
+ final String backupId = backup.getId().getBackup();
+ final com.google.spanner.admin.database.v1.Backup.Builder backupBuilder =
com.google.spanner.admin.database.v1.Backup.newBuilder()
.setDatabase(getDatabaseName(instanceId, databaseId))
- .setExpireTime(expireTime.toProto())
- .build();
- String instanceName = getInstanceName(instanceId);
- OperationFuture
- rawOperationFuture = rpc.createBackup(instanceName, backupId, backup);
+ .setExpireTime(backup.getExpireTime().toProto());
+ if (backup.getVersionTime() != null) {
+ backupBuilder.setVersionTime(backup.getVersionTime().toProto());
+ }
+ final String instanceName = getInstanceName(instanceId);
+ final OperationFuture
+ rawOperationFuture = rpc.createBackup(instanceName, backupId, backupBuilder.build());
- return new OperationFutureImpl(
+ return new OperationFutureImpl<>(
rawOperationFuture.getPollingFuture(),
rawOperationFuture.getInitialFuture(),
new ApiFunction() {
@@ -137,6 +152,7 @@ public Backup apply(OperationSnapshot snapshot) {
com.google.spanner.admin.database.v1.Backup.newBuilder(proto)
.setName(proto.getName())
.setExpireTime(proto.getExpireTime())
+ .setVersionTime(proto.getVersionTime())
.setState(proto.getState())
.build(),
DatabaseAdminClientImpl.this);
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 5b7faa324e..5ba9f0aa76 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
@@ -30,6 +30,10 @@ public abstract static class Builder {
abstract Builder setRestoreInfo(RestoreInfo restoreInfo);
+ abstract Builder setVersionRetentionPeriod(String versionRetentionPeriod);
+
+ abstract Builder setEarliestVersionTime(Timestamp earliestVersionTime);
+
abstract Builder setProto(com.google.spanner.admin.database.v1.Database proto);
/** Builds the database from this builder. */
@@ -41,6 +45,8 @@ abstract static class BuilderImpl extends Builder {
private State state = State.UNSPECIFIED;
private Timestamp createTime;
private RestoreInfo restoreInfo;
+ private String versionRetentionPeriod;
+ private Timestamp earliestVersionTime;
private com.google.spanner.admin.database.v1.Database proto;
BuilderImpl(DatabaseId id) {
@@ -52,6 +58,8 @@ abstract static class BuilderImpl extends Builder {
this.state = other.state;
this.createTime = other.createTime;
this.restoreInfo = other.restoreInfo;
+ this.versionRetentionPeriod = other.versionRetentionPeriod;
+ this.earliestVersionTime = other.earliestVersionTime;
this.proto = other.proto;
}
@@ -73,6 +81,18 @@ Builder setRestoreInfo(@Nullable RestoreInfo restoreInfo) {
return this;
}
+ @Override
+ Builder setVersionRetentionPeriod(String versionRetentionPeriod) {
+ this.versionRetentionPeriod = versionRetentionPeriod;
+ return this;
+ }
+
+ @Override
+ Builder setEarliestVersionTime(Timestamp earliestVersionTime) {
+ this.earliestVersionTime = earliestVersionTime;
+ return this;
+ }
+
@Override
Builder setProto(@Nullable com.google.spanner.admin.database.v1.Database proto) {
this.proto = proto;
@@ -96,6 +116,8 @@ public enum State {
private final State state;
private final Timestamp createTime;
private final RestoreInfo restoreInfo;
+ private final String versionRetentionPeriod;
+ private final Timestamp earliestVersionTime;
private final com.google.spanner.admin.database.v1.Database proto;
public DatabaseInfo(DatabaseId id, State state) {
@@ -103,6 +125,8 @@ public DatabaseInfo(DatabaseId id, State state) {
this.state = state;
this.createTime = null;
this.restoreInfo = null;
+ this.versionRetentionPeriod = null;
+ this.earliestVersionTime = null;
this.proto = null;
}
@@ -111,6 +135,8 @@ public DatabaseInfo(DatabaseId id, State state) {
this.state = builder.state;
this.createTime = builder.createTime;
this.restoreInfo = builder.restoreInfo;
+ this.versionRetentionPeriod = builder.versionRetentionPeriod;
+ this.earliestVersionTime = builder.earliestVersionTime;
this.proto = builder.proto;
}
@@ -129,6 +155,23 @@ public Timestamp getCreateTime() {
return createTime;
}
+ /**
+ * Returns the version retention period of the database. This is the period for which Cloud
+ * Spanner retains all versions of data for the database. For instance, if set to 3 days, Cloud
+ * Spanner will retain data versions that are up to 3 days old.
+ */
+ public String getVersionRetentionPeriod() {
+ return versionRetentionPeriod;
+ }
+
+ /**
+ * Returns the earliest version time of the database. This is the oldest timestamp that can be
+ * used to read old versions of the data.
+ */
+ public Timestamp getEarliestVersionTime() {
+ return earliestVersionTime;
+ }
+
/**
* Returns the {@link RestoreInfo} of the database if any is available, or null
if no
* {@link RestoreInfo} is available for this database.
@@ -154,16 +197,21 @@ public boolean equals(Object o) {
return id.equals(that.id)
&& state == that.state
&& Objects.equals(createTime, that.createTime)
- && Objects.equals(restoreInfo, that.restoreInfo);
+ && Objects.equals(restoreInfo, that.restoreInfo)
+ && Objects.equals(versionRetentionPeriod, that.versionRetentionPeriod)
+ && Objects.equals(earliestVersionTime, that.earliestVersionTime);
}
@Override
public int hashCode() {
- return Objects.hash(id, state, createTime, restoreInfo);
+ return Objects.hash(
+ id, state, createTime, restoreInfo, versionRetentionPeriod, earliestVersionTime);
}
@Override
public String toString() {
- return String.format("Database[%s, %s, %s, %s]", id.getName(), state, createTime, restoreInfo);
+ return String.format(
+ "Database[%s, %s, %s, %s, %s, %s]",
+ id.getName(), state, createTime, restoreInfo, versionRetentionPeriod, earliestVersionTime);
}
}
diff --git a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/testing/RemoteSpannerHelper.java b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/testing/RemoteSpannerHelper.java
index ef979a8706..6d2fe90227 100644
--- a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/testing/RemoteSpannerHelper.java
+++ b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/testing/RemoteSpannerHelper.java
@@ -30,6 +30,7 @@
import java.util.Arrays;
import java.util.List;
import java.util.Random;
+import java.util.concurrent.atomic.AtomicInteger;
import java.util.logging.Level;
import java.util.logging.Logger;
@@ -45,6 +46,8 @@ public class RemoteSpannerHelper {
private final InstanceId instanceId;
private static int dbSeq;
private static int dbPrefix = new Random().nextInt(Integer.MAX_VALUE);
+ private static final AtomicInteger backupSeq = new AtomicInteger();
+ private static int backupPrefix = new Random().nextInt(Integer.MAX_VALUE);
private final List dbs = new ArrayList<>();
protected RemoteSpannerHelper(SpannerOptions options, InstanceId instanceId, Spanner client) {
@@ -100,6 +103,13 @@ public String getUniqueDatabaseId() {
return String.format("testdb_%d_%04d", dbPrefix, dbSeq++);
}
+ /**
+ * Returns a backup id which is guaranteed to be unique within the context of this environment.
+ */
+ public String getUniqueBackupId() {
+ return String.format("testbck_%06d_%04d", backupPrefix, backupSeq.incrementAndGet());
+ }
+
/**
* Creates a test database defined by {@code statements} in the test instance. A {@code CREATE
* DATABASE ...} statement should not be included; an appropriate name will be chosen and the
diff --git a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/testing/TimestampHelper.java b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/testing/TimestampHelper.java
new file mode 100644
index 0000000000..021adf4dd7
--- /dev/null
+++ b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/testing/TimestampHelper.java
@@ -0,0 +1,41 @@
+/*
+ * Copyright 2021 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.
+ */
+
+package com.google.cloud.spanner.testing;
+
+import com.google.cloud.Timestamp;
+import java.util.concurrent.TimeUnit;
+
+public class TimestampHelper {
+
+ public static Timestamp daysAgo(int days) {
+ return Timestamp.ofTimeMicroseconds(
+ TimeUnit.MICROSECONDS.convert(System.currentTimeMillis(), TimeUnit.MILLISECONDS)
+ - TimeUnit.MICROSECONDS.convert(days, TimeUnit.DAYS));
+ }
+
+ public static Timestamp afterDays(int days) {
+ return Timestamp.ofTimeMicroseconds(
+ TimeUnit.MICROSECONDS.convert(System.currentTimeMillis(), TimeUnit.MILLISECONDS)
+ + TimeUnit.MICROSECONDS.convert(days, TimeUnit.DAYS));
+ }
+
+ public static Timestamp afterMinutes(int minutes) {
+ return Timestamp.ofTimeMicroseconds(
+ TimeUnit.MICROSECONDS.convert(System.currentTimeMillis(), TimeUnit.MILLISECONDS)
+ + TimeUnit.MICROSECONDS.convert(minutes, TimeUnit.MINUTES));
+ }
+}
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 3bdc673838..80b99f679b 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
@@ -46,6 +46,7 @@ public class BackupTest {
"projects/test-project/instances/test-instance/backups/backup-1";
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);
@Mock DatabaseAdminClient dbClient;
@@ -65,11 +66,13 @@ public Builder answer(InvocationOnMock invocation) {
@Test
public void build() {
Timestamp expireTime = Timestamp.now();
+ Timestamp versionTime = Timestamp.ofTimeMicroseconds(10L);
Backup backup =
dbClient
.newBackupBuilder(BackupId.of("test-project", "instance-id", "backup-id"))
.setDatabase(DatabaseId.of("test-project", "instance-id", "src-database"))
.setExpireTime(expireTime)
+ .setVersionTime(versionTime)
.setSize(100L)
.setState(State.CREATING)
.build();
@@ -77,6 +80,7 @@ public void build() {
assertThat(copy.getId()).isEqualTo(backup.getId());
assertThat(copy.getDatabase()).isEqualTo(backup.getDatabase());
assertThat(copy.getExpireTime()).isEqualTo(backup.getExpireTime());
+ assertThat(copy.getVersionTime()).isEqualTo(backup.getVersionTime());
assertThat(copy.getSize()).isEqualTo(backup.getSize());
assertThat(copy.getState()).isEqualTo(backup.getState());
}
@@ -84,14 +88,16 @@ public void build() {
@Test
public void create() {
Timestamp expireTime = Timestamp.now();
+ Timestamp versionTime = Timestamp.ofTimeMicroseconds(10L);
Backup backup =
dbClient
.newBackupBuilder(BackupId.of("test-project", "instance-id", "backup-id"))
.setDatabase(DatabaseId.of("test-project", "instance-id", "src-database"))
.setExpireTime(expireTime)
+ .setVersionTime(versionTime)
.build();
backup.create();
- verify(dbClient).createBackup("instance-id", "backup-id", "src-database", expireTime);
+ verify(dbClient).createBackup(backup);
}
@Test
@@ -125,6 +131,19 @@ public void createWithoutExpireTime() {
}
}
+ @Test
+ public void createWithoutVersionTimeShouldSucceed() {
+ final Timestamp expireTime = Timestamp.now();
+ Backup backup =
+ dbClient
+ .newBackupBuilder(BackupId.of("test-project", "instance-id", "backup-id"))
+ .setDatabase(DatabaseId.of("test-project", "instance-id", "src-database"))
+ .setExpireTime(expireTime)
+ .build();
+ backup.create();
+ verify(dbClient).createBackup(backup);
+ }
+
@Test
public void exists() {
when(dbClient.getBackup("test-instance", "test-backup"))
@@ -298,6 +317,7 @@ public void fromProto() {
assertThat(backup.getId().getName()).isEqualTo(NAME);
assertThat(backup.getState()).isEqualTo(BackupInfo.State.CREATING);
assertThat(backup.getExpireTime()).isEqualTo(EXP_TIME);
+ assertThat(backup.getVersionTime()).isEqualTo(VERSION_TIME);
}
private Backup createBackup() {
@@ -307,6 +327,8 @@ private Backup createBackup() {
.setDatabase(DB)
.setExpireTime(
com.google.protobuf.Timestamp.newBuilder().setSeconds(1000L).setNanos(1000).build())
+ .setVersionTime(
+ com.google.protobuf.Timestamp.newBuilder().setSeconds(2000L).setNanos(2000).build())
.setState(com.google.spanner.admin.database.v1.Backup.State.CREATING)
.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 b2676473cf..fb617797aa 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
@@ -48,6 +48,7 @@
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
+import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeUnit;
import org.junit.Assert;
import org.junit.Before;
@@ -69,6 +70,8 @@ public class DatabaseAdminClientImplTest {
private static final String BK_ID = "my-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();
+ private static final String VERSION_RETENTION_PERIOD = "7d";
@Mock SpannerRpc rpc;
DatabaseAdminClientImpl client;
@@ -80,7 +83,12 @@ public void setUp() {
}
private Database getDatabaseProto() {
- return Database.newBuilder().setName(DB_NAME).setState(Database.State.READY).build();
+ return Database.newBuilder()
+ .setName(DB_NAME)
+ .setState(Database.State.READY)
+ .setEarliestVersionTime(EARLIEST_VERSION_TIME.toProto())
+ .setVersionRetentionPeriod(VERSION_RETENTION_PERIOD)
+ .build();
}
private Database getAnotherDatabaseProto() {
@@ -116,6 +124,8 @@ public void getDatabase() {
com.google.cloud.spanner.Database db = client.getDatabase(INSTANCE_ID, DB_ID);
assertThat(db.getId().getName()).isEqualTo(DB_NAME);
assertThat(db.getState()).isEqualTo(DatabaseInfo.State.READY);
+ assertThat(db.getEarliestVersionTime()).isEqualTo(EARLIEST_VERSION_TIME);
+ assertThat(db.getVersionRetentionPeriod()).isEqualTo(VERSION_RETENTION_PERIOD);
}
@Test
@@ -292,7 +302,7 @@ public void testDatabaseIAMPermissions() {
}
@Test
- public void createBackup() throws Exception {
+ public void createBackupWithParams() throws Exception {
OperationFuture rawOperationFuture =
OperationFutureUtil.immediateOperationFuture(
"createBackup", getBackupProto(), CreateBackupMetadata.getDefaultInstance());
@@ -308,6 +318,40 @@ public void createBackup() throws Exception {
assertThat(op.get().getId().getName()).isEqualTo(BK_NAME);
}
+ @Test
+ public void createBackupWithBackupObject() throws ExecutionException, InterruptedException {
+ final OperationFuture rawOperationFuture =
+ OperationFutureUtil.immediateOperationFuture(
+ "createBackup", getBackupProto(), CreateBackupMetadata.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 Backup expectedCallBackup =
+ Backup.newBuilder()
+ .setDatabase(DB_NAME)
+ .setExpireTime(expireTime.toProto())
+ .setVersionTime(versionTime.toProto())
+ .build();
+ final com.google.cloud.spanner.Backup requestBackup =
+ client
+ .newBackupBuilder(BackupId.of(PROJECT_ID, INSTANCE_ID, BK_ID))
+ .setDatabase(DatabaseId.of(PROJECT_ID, INSTANCE_ID, DB_ID))
+ .setExpireTime(expireTime)
+ .setVersionTime(versionTime)
+ .build();
+
+ when(rpc.createBackup(INSTANCE_NAME, BK_ID, expectedCallBackup)).thenReturn(rawOperationFuture);
+
+ final OperationFuture op =
+ client.createBackup(requestBackup);
+ assertThat(op.isDone()).isTrue();
+ assertThat(op.get().getId().getName()).isEqualTo(BK_NAME);
+ }
+
@Test
public void deleteBackup() {
client.deleteBackup(INSTANCE_ID, BK_ID);
diff --git a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/DatabaseAdminClientTest.java b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/DatabaseAdminClientTest.java
index 8b30df912d..95641ef3ff 100644
--- a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/DatabaseAdminClientTest.java
+++ b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/DatabaseAdminClientTest.java
@@ -16,6 +16,7 @@
package com.google.cloud.spanner;
+import static com.google.cloud.spanner.testing.TimestampHelper.afterDays;
import static com.google.common.truth.Truth.assertThat;
import static org.junit.Assert.fail;
@@ -224,7 +225,7 @@ public void tearDown() {
public void dbAdminCreateBackup() throws InterruptedException, ExecutionException {
final String backupId = "other-backup-id";
OperationFuture op =
- client.createBackup(INSTANCE_ID, backupId, DB_ID, after7Days());
+ client.createBackup(INSTANCE_ID, backupId, DB_ID, afterDays(7));
Backup backup = op.get();
assertThat(backup.getId().getName())
.isEqualTo(
@@ -240,7 +241,8 @@ public void backupCreate() throws InterruptedException, ExecutionException {
client
.newBackupBuilder(BackupId.of(PROJECT_ID, INSTANCE_ID, backupId))
.setDatabase(DatabaseId.of(PROJECT_ID, INSTANCE_ID, DB_ID))
- .setExpireTime(after7Days())
+ .setExpireTime(afterDays(7))
+ .setVersionTime(sevenDaysAgo())
.build();
OperationFuture op = backup.create();
backup = op.get();
@@ -251,6 +253,25 @@ public void backupCreate() throws InterruptedException, ExecutionException {
assertThat(client.getBackup(INSTANCE_ID, backupId)).isEqualTo(backup);
}
+ @Test
+ public void databaseAdminBackupCreate() throws ExecutionException, InterruptedException {
+ final String backupId = "other-backup-id";
+ Backup backup =
+ client
+ .newBackupBuilder(BackupId.of(PROJECT_ID, INSTANCE_ID, backupId))
+ .setDatabase(DatabaseId.of(PROJECT_ID, INSTANCE_ID, DB_ID))
+ .setExpireTime(afterDays(7))
+ .setVersionTime(sevenDaysAgo())
+ .build();
+ final OperationFuture op = client.createBackup(backup);
+ backup = op.get();
+ assertThat(backup.getId().getName())
+ .isEqualTo(
+ String.format(
+ "projects/%s/instances/%s/backups/%s", PROJECT_ID, INSTANCE_ID, backupId));
+ assertThat(client.getBackup(INSTANCE_ID, backupId)).isEqualTo(backup);
+ }
+
@Test
public void backupCreateCancel() {
final String backupId = "other-backup-id";
@@ -299,7 +320,7 @@ public void databaseBackup() throws InterruptedException, ExecutionException {
db.backup(
client
.newBackupBuilder(BackupId.of(PROJECT_ID, INSTANCE_ID, backupId))
- .setExpireTime(after7Days())
+ .setExpireTime(afterDays(7))
.build())
.get();
assertThat(backup.getId().getName())
@@ -312,7 +333,7 @@ public void databaseBackup() throws InterruptedException, ExecutionException {
@Test
public void dbAdminCreateBackupAlreadyExists() throws InterruptedException {
OperationFuture op =
- client.createBackup(INSTANCE_ID, BCK_ID, DB_ID, after7Days());
+ client.createBackup(INSTANCE_ID, BCK_ID, DB_ID, afterDays(7));
try {
op.get();
fail("missing expected exception");
@@ -329,7 +350,7 @@ public void backupCreateAlreadyExists() throws InterruptedException {
client
.newBackupBuilder(BackupId.of(PROJECT_ID, INSTANCE_ID, BCK_ID))
.setDatabase(DatabaseId.of(PROJECT_ID, INSTANCE_ID, DB_ID))
- .setExpireTime(after7Days())
+ .setExpireTime(afterDays(7))
.build();
try {
backup.create().get();
@@ -348,7 +369,7 @@ public void databaseBackupAlreadyExists() throws InterruptedException {
db.backup(
client
.newBackupBuilder(BackupId.of(PROJECT_ID, INSTANCE_ID, BCK_ID))
- .setExpireTime(after7Days())
+ .setExpireTime(afterDays(7))
.build());
try {
op.get();
@@ -364,7 +385,7 @@ public void databaseBackupAlreadyExists() throws InterruptedException {
public void dbAdminCreateBackupDbNotFound() throws InterruptedException {
final String backupId = "other-backup-id";
OperationFuture op =
- client.createBackup(INSTANCE_ID, backupId, "does-not-exist", after7Days());
+ client.createBackup(INSTANCE_ID, backupId, "does-not-exist", afterDays(7));
try {
op.get();
fail("missing expected exception");
@@ -381,7 +402,7 @@ public void backupCreateDbNotFound() throws InterruptedException {
client
.newBackupBuilder(BackupId.of(PROJECT_ID, INSTANCE_ID, backupId))
.setDatabase(DatabaseId.of(PROJECT_ID, INSTANCE_ID, "does-not-exist"))
- .setExpireTime(after7Days())
+ .setExpireTime(afterDays(7))
.build();
try {
backup.create().get();
@@ -402,7 +423,7 @@ public void databaseBackupDbNotFound() throws InterruptedException {
db.backup(
client
.newBackupBuilder(BackupId.of(PROJECT_ID, INSTANCE_ID, backupId))
- .setExpireTime(after7Days())
+ .setExpireTime(afterDays(7))
.build());
try {
op.get();
@@ -499,7 +520,7 @@ public void dbClientListBackups()
throws SpannerException, InterruptedException, ExecutionException {
Backup backup = client.getBackup(INSTANCE_ID, BCK_ID);
assertThat(client.listBackups(INSTANCE_ID).iterateAll()).containsExactly(backup);
- Backup backup2 = client.createBackup(INSTANCE_ID, "backup2", DB_ID, after7Days()).get();
+ Backup backup2 = client.createBackup(INSTANCE_ID, "backup2", DB_ID, afterDays(7)).get();
assertThat(client.listBackups(INSTANCE_ID).iterateAll()).containsExactly(backup, backup2);
backup2.delete();
assertThat(client.listBackups(INSTANCE_ID).iterateAll()).containsExactly(backup);
@@ -515,7 +536,7 @@ public void instanceListBackups()
.build();
Backup backup = client.getBackup(INSTANCE_ID, BCK_ID);
assertThat(instance.listBackups().iterateAll()).containsExactly(backup);
- Backup backup2 = client.createBackup(INSTANCE_ID, "backup2", DB_ID, after7Days()).get();
+ Backup backup2 = client.createBackup(INSTANCE_ID, "backup2", DB_ID, afterDays(7)).get();
assertThat(instance.listBackups().iterateAll()).containsExactly(backup, backup2);
backup2.delete();
assertThat(instance.listBackups().iterateAll()).containsExactly(backup);
@@ -532,7 +553,7 @@ public void instanceListBackupsWithFilter()
Backup backup = client.getBackup(INSTANCE_ID, BCK_ID);
assertThat(instance.listBackups().iterateAll()).containsExactly(backup);
- Backup backup2 = client.createBackup(INSTANCE_ID, "backup2", DB_ID, after7Days()).get();
+ Backup backup2 = client.createBackup(INSTANCE_ID, "backup2", DB_ID, afterDays(7)).get();
// All backups.
assertThat(instance.listBackups().iterateAll()).containsExactly(backup, backup2);
@@ -549,7 +570,7 @@ public void instanceListBackupsWithFilter()
.containsExactly(backup, backup2);
// All backups that expire before a certain time.
- String ts = after14Days().toString();
+ String ts = afterDays(14).toString();
filter = String.format("expire_time < \"%s\"", ts);
mockDatabaseAdmin.addFilterMatches(filter, backup.getId().getName(), backup2.getId().getName());
assertThat(instance.listBackups(Options.filter(filter)).iterateAll())
@@ -701,7 +722,7 @@ public void databaseListDatabaseOperations()
public void dbClientListBackupOperations()
throws SpannerException, InterruptedException, ExecutionException {
assertThat(client.listBackupOperations(INSTANCE_ID).iterateAll()).hasSize(1);
- client.createBackup(INSTANCE_ID, "other-backup", DB_ID, after7Days()).get();
+ client.createBackup(INSTANCE_ID, "other-backup", DB_ID, afterDays(7)).get();
assertThat(client.listBackupOperations(INSTANCE_ID).iterateAll()).hasSize(2);
// Restore a backup. This creates 2 DATABASE operations: One to restore the database and
// one to optimize it.
@@ -723,7 +744,7 @@ public void instanceListBackupOperations()
.backup(
client
.newBackupBuilder(BackupId.of(PROJECT_ID, INSTANCE_ID, "other-backup"))
- .setExpireTime(after7Days())
+ .setExpireTime(afterDays(7))
.build())
.get();
assertThat(instance.listBackupOperations().iterateAll()).hasSize(2);
@@ -768,7 +789,7 @@ public void backupListBackupOperations()
Backup backup = client.newBackupBuilder(BackupId.of(PROJECT_ID, INSTANCE_ID, BCK_ID)).build();
mockDatabaseAdmin.addFilterMatches("name:backups/" + BCK_ID, createBackupOperation.getName());
assertThat(backup.listBackupOperations().iterateAll()).hasSize(1);
- client.createBackup(INSTANCE_ID, "other-backup", DB_ID, after7Days()).get();
+ client.createBackup(INSTANCE_ID, "other-backup", DB_ID, afterDays(7)).get();
assertThat(backup.listBackupOperations().iterateAll()).hasSize(1);
}
@@ -791,16 +812,10 @@ public void testDatabaseIAMPermissions() {
assertThat(permissions).containsExactly("spanner.databases.select");
}
- private Timestamp after7Days() {
- return Timestamp.ofTimeMicroseconds(
- TimeUnit.MICROSECONDS.convert(System.currentTimeMillis(), TimeUnit.MILLISECONDS)
- + TimeUnit.MICROSECONDS.convert(7, TimeUnit.DAYS));
- }
-
- private Timestamp after14Days() {
+ private Timestamp sevenDaysAgo() {
return Timestamp.ofTimeMicroseconds(
TimeUnit.MICROSECONDS.convert(System.currentTimeMillis(), TimeUnit.MILLISECONDS)
- + TimeUnit.MICROSECONDS.convert(14, TimeUnit.DAYS));
+ - TimeUnit.MICROSECONDS.convert(7, TimeUnit.DAYS));
}
private void createTestDatabase() {
@@ -814,7 +829,7 @@ private void createTestDatabase() {
private void createTestBackup() {
try {
- createBackupOperation = client.createBackup(INSTANCE_ID, BCK_ID, DB_ID, after7Days());
+ createBackupOperation = client.createBackup(INSTANCE_ID, BCK_ID, DB_ID, afterDays(7));
createBackupOperation.get();
} catch (InterruptedException | ExecutionException e) {
throw SpannerExceptionFactory.newSpannerException(e);
@@ -839,7 +854,7 @@ public void retryCreateBackupSlowResponse() throws Exception {
SimulatedExecutionTime.ofException(Status.DEADLINE_EXCEEDED.asRuntimeException()));
final String backupId = "other-backup-id";
OperationFuture op =
- client.createBackup(INSTANCE_ID, backupId, DB_ID, after7Days());
+ client.createBackup(INSTANCE_ID, backupId, DB_ID, afterDays(7));
Backup backup = op.get();
assertThat(backup.getId().getName())
.isEqualTo(
@@ -857,7 +872,7 @@ public void retryCreateBackupSlowStartup() throws Exception {
SimulatedExecutionTime.ofException(Status.DEADLINE_EXCEEDED.asRuntimeException()));
final String backupId = "other-backup-id";
OperationFuture op =
- client.createBackup(INSTANCE_ID, backupId, DB_ID, after7Days());
+ client.createBackup(INSTANCE_ID, backupId, DB_ID, afterDays(7));
Backup backup = op.get();
assertThat(backup.getId().getName())
.isEqualTo(
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 9557615007..7c61720b21 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
@@ -42,6 +42,9 @@ public class DatabaseTest {
private static final String NAME =
"projects/test-project/instances/test-instance/databases/database-1";
+ private static final Timestamp EARLIEST_VERSION_TIME = Timestamp.now();
+ private static final String VERSION_RETENTION_PERIOD = "7d";
+
@Mock DatabaseAdminClient dbClient;
@Before
@@ -61,12 +64,13 @@ public Backup.Builder answer(InvocationOnMock invocation) {
public void backup() {
Timestamp expireTime = Timestamp.now();
Database db = createDatabase();
- db.backup(
+ Backup backup =
dbClient
.newBackupBuilder(BackupId.of("test-project", "test-instance", "test-backup"))
.setExpireTime(expireTime)
- .build());
- verify(dbClient).createBackup("test-instance", "test-backup", "database-1", expireTime);
+ .build();
+ db.backup(backup);
+ verify(dbClient).createBackup(backup.toBuilder().setDatabase(db.getId()).build());
}
@Test
@@ -82,6 +86,8 @@ public void fromProto() {
Database db = createDatabase();
assertThat(db.getId().getName()).isEqualTo(NAME);
assertThat(db.getState()).isEqualTo(DatabaseInfo.State.CREATING);
+ assertThat(db.getVersionRetentionPeriod()).isEqualTo(VERSION_RETENTION_PERIOD);
+ assertThat(db.getEarliestVersionTime()).isEqualTo(EARLIEST_VERSION_TIME);
}
@Test
@@ -119,6 +125,8 @@ private Database createDatabase() {
com.google.spanner.admin.database.v1.Database.newBuilder()
.setName(NAME)
.setState(com.google.spanner.admin.database.v1.Database.State.CREATING)
+ .setEarliestVersionTime(EARLIEST_VERSION_TIME.toProto())
+ .setVersionRetentionPeriod(VERSION_RETENTION_PERIOD)
.build();
return Database.fromProto(proto, dbClient);
}
diff --git a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/it/ITBackupTest.java b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/it/ITBackupTest.java
index 0fbd0702ec..e9b9887184 100644
--- a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/it/ITBackupTest.java
+++ b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/it/ITBackupTest.java
@@ -17,6 +17,9 @@
package com.google.cloud.spanner.it;
import static com.google.cloud.spanner.testing.EmulatorSpannerHelper.isUsingEmulator;
+import static com.google.cloud.spanner.testing.TimestampHelper.afterDays;
+import static com.google.cloud.spanner.testing.TimestampHelper.afterMinutes;
+import static com.google.cloud.spanner.testing.TimestampHelper.daysAgo;
import static com.google.common.truth.Truth.assertThat;
import static org.junit.Assert.fail;
import static org.junit.Assume.assumeFalse;
@@ -58,7 +61,6 @@
import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
-import java.util.concurrent.atomic.AtomicInteger;
import java.util.logging.Level;
import java.util.logging.Logger;
import java.util.regex.Matcher;
@@ -87,7 +89,6 @@ public class ITBackupTest {
private InstanceAdminClient instanceAdminClient;
private Instance instance;
private RemoteSpannerHelper testHelper;
- private final AtomicInteger backupSeq = new AtomicInteger();
private List databases = new ArrayList<>();
private List backups = new ArrayList<>();
private final Random random = new Random();
@@ -188,34 +189,6 @@ private void waitForDbOperations(String backupId) throws InterruptedException {
}
}
- private String getUniqueBackupId() {
- return String.format("testbck_%06d_%04d", random.nextInt(1000000), backupSeq.incrementAndGet());
- }
-
- private static Timestamp after7Days() {
- return Timestamp.ofTimeMicroseconds(
- TimeUnit.MICROSECONDS.convert(System.currentTimeMillis(), TimeUnit.MILLISECONDS)
- + TimeUnit.MICROSECONDS.convert(7L, TimeUnit.DAYS));
- }
-
- private Timestamp after5Minutes() {
- return Timestamp.ofTimeMicroseconds(
- TimeUnit.MICROSECONDS.convert(System.currentTimeMillis(), TimeUnit.MILLISECONDS)
- + TimeUnit.MICROSECONDS.convert(5L, TimeUnit.MINUTES));
- }
-
- private Timestamp tomorrow() {
- return Timestamp.ofTimeMicroseconds(
- TimeUnit.MICROSECONDS.convert(System.currentTimeMillis(), TimeUnit.MILLISECONDS)
- + TimeUnit.MICROSECONDS.convert(1L, TimeUnit.DAYS));
- }
-
- private Timestamp yesterday() {
- return Timestamp.ofTimeMicroseconds(
- TimeUnit.MICROSECONDS.convert(System.currentTimeMillis(), TimeUnit.MILLISECONDS)
- - TimeUnit.MICROSECONDS.convert(1L, TimeUnit.DAYS));
- }
-
@Test
public void testBackups() throws InterruptedException, ExecutionException {
// Create two test databases in parallel.
@@ -250,9 +223,9 @@ public void testBackups() throws InterruptedException, ExecutionException {
.build()));
// Create two backups in parallel.
- String backupId1 = getUniqueBackupId() + "_bck1";
- String backupId2 = getUniqueBackupId() + "_bck2";
- Timestamp expireTime = after7Days();
+ String backupId1 = testHelper.getUniqueBackupId() + "_bck1";
+ String backupId2 = testHelper.getUniqueBackupId() + "_bck2";
+ Timestamp expireTime = afterDays(7);
logger.info(String.format("Creating backups %s and %s in parallel", backupId1, backupId2));
OperationFuture op1 =
dbAdminClient.createBackup(
@@ -414,8 +387,8 @@ private void testMetadata(
private void testCreateInvalidExpirationDate(Database db) throws InterruptedException {
// This is not allowed, the expiration date must be at least 6 hours in the future.
- Timestamp expireTime = yesterday();
- String backupId = getUniqueBackupId();
+ Timestamp expireTime = daysAgo(1);
+ String backupId = testHelper.getUniqueBackupId();
logger.info(String.format("Creating backup %s with invalid expiration date", backupId));
OperationFuture op =
dbAdminClient.createBackup(
@@ -437,8 +410,8 @@ private void testCreateInvalidExpirationDate(Database db) throws InterruptedExce
private void testCancelBackupOperation(Database db)
throws InterruptedException, ExecutionException {
- Timestamp expireTime = after7Days();
- String backupId = getUniqueBackupId();
+ Timestamp expireTime = afterDays(7);
+ String backupId = testHelper.getUniqueBackupId();
logger.info(String.format("Starting to create backup %s", backupId));
OperationFuture op =
dbAdminClient.createBackup(
@@ -477,7 +450,7 @@ private void testGetBackup(Database db, String backupId, Timestamp expireTime) {
private void testUpdateBackup(Backup backup) {
// Update the expire time.
- Timestamp tomorrow = tomorrow();
+ Timestamp tomorrow = afterDays(1);
backup = backup.toBuilder().setExpireTime(tomorrow).build();
logger.info(
String.format("Updating expire time of backup %s to 1 week", backup.getId().getBackup()));
@@ -488,7 +461,7 @@ private void testUpdateBackup(Backup backup) {
assertThat(backup.getExpireTime()).isEqualTo(tomorrow);
// Try to set the expire time to 5 minutes in the future.
- Timestamp in5Minutes = after5Minutes();
+ Timestamp in5Minutes = afterMinutes(5);
backup = backup.toBuilder().setExpireTime(in5Minutes).build();
try {
logger.info(
diff --git a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/it/ITPitrBackupAndRestore.java b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/it/ITPitrBackupAndRestore.java
new file mode 100644
index 0000000000..bfdaa60a25
--- /dev/null
+++ b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/it/ITPitrBackupAndRestore.java
@@ -0,0 +1,244 @@
+/*
+ * Copyright 2021 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.
+ */
+
+package com.google.cloud.spanner.it;
+
+import static com.google.cloud.spanner.testing.EmulatorSpannerHelper.isUsingEmulator;
+import static com.google.cloud.spanner.testing.TimestampHelper.afterDays;
+import static com.google.cloud.spanner.testing.TimestampHelper.daysAgo;
+import static com.google.common.truth.Truth.assertThat;
+import static org.junit.Assume.assumeFalse;
+
+import com.google.api.gax.longrunning.OperationFuture;
+import com.google.api.gax.paging.Page;
+import com.google.cloud.Timestamp;
+import com.google.cloud.spanner.Backup;
+import com.google.cloud.spanner.BackupId;
+import com.google.cloud.spanner.Database;
+import com.google.cloud.spanner.DatabaseAdminClient;
+import com.google.cloud.spanner.DatabaseId;
+import com.google.cloud.spanner.InstanceId;
+import com.google.cloud.spanner.IntegrationTestEnv;
+import com.google.cloud.spanner.ParallelIntegrationTest;
+import com.google.cloud.spanner.SpannerException;
+import com.google.cloud.spanner.testing.RemoteSpannerHelper;
+import com.google.spanner.admin.database.v1.CreateDatabaseMetadata;
+import com.google.spanner.admin.database.v1.RestoreDatabaseMetadata;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.TimeoutException;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+import org.junit.AfterClass;
+import org.junit.BeforeClass;
+import org.junit.ClassRule;
+import org.junit.Ignore;
+import org.junit.Test;
+import org.junit.experimental.categories.Category;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+@Category(ParallelIntegrationTest.class)
+@RunWith(JUnit4.class)
+public class ITPitrBackupAndRestore {
+ private static final Logger logger = Logger.getLogger(ITPitrBackupAndRestore.class.getName());
+
+ @ClassRule public static IntegrationTestEnv env = new IntegrationTestEnv();
+ private static final long OP_TIMEOUT = 10;
+ private static final TimeUnit OP_TIMEOUT_UNIT = TimeUnit.MINUTES;
+ private static RemoteSpannerHelper testHelper;
+ private static DatabaseAdminClient dbAdminClient;
+ private static Database testDatabase;
+ private static final List backupsToDrop = new ArrayList<>();
+ private static final List databasesToDrop = new ArrayList<>();
+
+ @BeforeClass
+ public static void doNotRunOnEmulator() {
+ assumeFalse("PITR features are not supported by the emulator", isUsingEmulator());
+ }
+
+ @BeforeClass
+ public static void setUp() throws Exception {
+ testHelper = env.getTestHelper();
+ dbAdminClient = testHelper.getClient().getDatabaseAdminClient();
+ testDatabase = createTestDatabase();
+ }
+
+ @AfterClass
+ public static void tearDown() {
+ int numDropped = 0;
+ for (Database database : databasesToDrop) {
+ try {
+ database.drop();
+ numDropped++;
+ } catch (SpannerException e) {
+ logger.log(Level.SEVERE, "Failed to drop test database " + database.getId(), e);
+ }
+ }
+ logger.log(Level.INFO, "Dropped {0} test databases(s)", numDropped);
+
+ numDropped = 0;
+ for (Backup backup : backupsToDrop) {
+ try {
+ backup.delete();
+ numDropped++;
+ } catch (SpannerException e) {
+ logger.log(Level.SEVERE, "Failed to drop test backup " + backup.getId(), e);
+ }
+ }
+ logger.log(Level.INFO, "Dropped {0} test backup(s)", numDropped);
+ }
+
+ @Test
+ @Ignore("backup and restore for pitr is not released yet")
+ public void backupCreationWithVersionTimeWithinVersionRetentionPeriodSucceeds() throws Exception {
+ final DatabaseId backupDatabaseId = testDatabase.getId();
+ final String restoreDatabaseId = testHelper.getUniqueDatabaseId();
+ final String projectId = backupDatabaseId.getInstanceId().getProject();
+ final String instanceId = backupDatabaseId.getInstanceId().getInstance();
+ final String backupId = testHelper.getUniqueBackupId();
+ final Timestamp expireTime = afterDays(7);
+ final Timestamp versionTime = testDatabase.getEarliestVersionTime();
+ final Backup backupToCreate =
+ dbAdminClient
+ .newBackupBuilder(BackupId.of(projectId, instanceId, backupId))
+ .setDatabase(backupDatabaseId)
+ .setExpireTime(expireTime)
+ .setVersionTime(versionTime)
+ .build();
+
+ final Backup createdBackup = createBackup(backupToCreate);
+ assertThat(createdBackup.getVersionTime()).isEqualTo(versionTime);
+
+ final RestoreDatabaseMetadata restoreDatabaseMetadata =
+ restoreDatabase(instanceId, backupId, restoreDatabaseId);
+ assertThat(Timestamp.fromProto(restoreDatabaseMetadata.getBackupInfo().getVersionTime()))
+ .isEqualTo(versionTime);
+
+ final Database retrievedDatabase = dbAdminClient.getDatabase(instanceId, restoreDatabaseId);
+ assertThat(retrievedDatabase).isNotNull();
+ assertThat(
+ Timestamp.fromProto(
+ retrievedDatabase.getRestoreInfo().getProto().getBackupInfo().getVersionTime()))
+ .isEqualTo(versionTime);
+
+ final Database listedDatabase = listDatabase(instanceId, restoreDatabaseId);
+ assertThat(listedDatabase).isNotNull();
+ assertThat(
+ Timestamp.fromProto(
+ listedDatabase.getRestoreInfo().getProto().getBackupInfo().getVersionTime()))
+ .isEqualTo(versionTime);
+ }
+
+ @Test(expected = SpannerException.class)
+ @Ignore("backup and restore for pitr is not released yet")
+ public void backupCreationWithVersionTimeTooFarInThePastFails() throws Exception {
+ final DatabaseId databaseId = testDatabase.getId();
+ final InstanceId instanceId = databaseId.getInstanceId();
+ final String backupId = testHelper.getUniqueBackupId();
+ final Timestamp expireTime = afterDays(7);
+ final Timestamp versionTime = daysAgo(30);
+ final Backup backupToCreate =
+ dbAdminClient
+ .newBackupBuilder(BackupId.of(instanceId, backupId))
+ .setDatabase(databaseId)
+ .setExpireTime(expireTime)
+ .setVersionTime(versionTime)
+ .build();
+
+ createBackup(backupToCreate);
+ }
+
+ @Test(expected = SpannerException.class)
+ @Ignore("backup and restore for pitr is not released yet")
+ public void backupCreationWithVersionTimeInTheFutureFails() throws Exception {
+ final DatabaseId databaseId = testDatabase.getId();
+ final InstanceId instanceId = databaseId.getInstanceId();
+ final String backupId = testHelper.getUniqueBackupId();
+ final Timestamp expireTime = afterDays(7);
+ final Timestamp versionTime = afterDays(1);
+ final Backup backupToCreate =
+ dbAdminClient
+ .newBackupBuilder(BackupId.of(instanceId, backupId))
+ .setDatabase(databaseId)
+ .setExpireTime(expireTime)
+ .setVersionTime(versionTime)
+ .build();
+
+ createBackup(backupToCreate);
+ }
+
+ private Backup createBackup(Backup backupToCreate)
+ throws InterruptedException, ExecutionException, TimeoutException {
+ final Backup createdBackup = getOrThrow(dbAdminClient.createBackup(backupToCreate));
+ backupsToDrop.add(createdBackup);
+ return createdBackup;
+ }
+
+ private RestoreDatabaseMetadata restoreDatabase(
+ String instanceId, String backupId, String databaseId)
+ throws InterruptedException, ExecutionException, TimeoutException {
+ final OperationFuture op =
+ dbAdminClient.restoreDatabase(instanceId, backupId, instanceId, databaseId);
+ final Database database = getOrThrow(op);
+ databasesToDrop.add(database);
+ return op.getMetadata().get(OP_TIMEOUT, OP_TIMEOUT_UNIT);
+ }
+
+ private Database listDatabase(String instanceId, String databaseId) {
+ Page page = dbAdminClient.listDatabases(instanceId);
+ while (page != null) {
+ for (Database database : page.getValues()) {
+ if (database.getId().getDatabase().equals(databaseId)) {
+ return database;
+ }
+ }
+ page = page.getNextPage();
+ }
+ return null;
+ }
+
+ private static Database createTestDatabase()
+ throws InterruptedException, ExecutionException, TimeoutException {
+ final String instanceId = testHelper.getInstanceId().getInstance();
+ final String databaseId = testHelper.getUniqueDatabaseId();
+ final OperationFuture op =
+ dbAdminClient.createDatabase(
+ instanceId,
+ databaseId,
+ Collections.singletonList(
+ "ALTER DATABASE " + databaseId + " SET OPTIONS (version_retention_period = '7d')"));
+ final Database database = getOrThrow(op);
+ databasesToDrop.add(database);
+ return database;
+ }
+
+ private static T getOrThrow(OperationFuture op)
+ throws TimeoutException, InterruptedException, ExecutionException {
+ try {
+ return op.get(OP_TIMEOUT, OP_TIMEOUT_UNIT);
+ } catch (ExecutionException e) {
+ if (e.getCause() != null && e.getCause() instanceof SpannerException) {
+ throw (SpannerException) e.getCause();
+ } else {
+ throw e;
+ }
+ }
+ }
+}
diff --git a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/it/ITPitrCreateDatabaseTest.java b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/it/ITPitrCreateDatabaseTest.java
new file mode 100644
index 0000000000..f9fa081e41
--- /dev/null
+++ b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/it/ITPitrCreateDatabaseTest.java
@@ -0,0 +1,144 @@
+/*
+ * Copyright 2020 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.
+ */
+
+package com.google.cloud.spanner.it;
+
+import static com.google.cloud.spanner.testing.EmulatorSpannerHelper.isUsingEmulator;
+import static com.google.common.truth.Truth.assertThat;
+import static org.junit.Assert.fail;
+import static org.junit.Assume.assumeFalse;
+
+import com.google.cloud.spanner.Database;
+import com.google.cloud.spanner.DatabaseAdminClient;
+import com.google.cloud.spanner.DatabaseId;
+import com.google.cloud.spanner.DatabaseNotFoundException;
+import com.google.cloud.spanner.ErrorCode;
+import com.google.cloud.spanner.IntegrationTestEnv;
+import com.google.cloud.spanner.ParallelIntegrationTest;
+import com.google.cloud.spanner.SpannerException;
+import com.google.cloud.spanner.testing.RemoteSpannerHelper;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.concurrent.TimeUnit;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.BeforeClass;
+import org.junit.ClassRule;
+import org.junit.Test;
+import org.junit.experimental.categories.Category;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+import org.threeten.bp.Duration;
+
+@Category(ParallelIntegrationTest.class)
+@RunWith(JUnit4.class)
+public class ITPitrCreateDatabaseTest {
+
+ private static final Duration OPERATION_TIMEOUT = Duration.ofMinutes(2);
+ private static final String VERSION_RETENTION_PERIOD = "7d";
+
+ @ClassRule public static IntegrationTestEnv env = new IntegrationTestEnv();
+ private RemoteSpannerHelper testHelper;
+ private DatabaseAdminClient dbAdminClient;
+ private List databasesToDrop;
+
+ @BeforeClass
+ public static void doNotRunOnEmulator() {
+ assumeFalse("PITR-lite features are not supported by the emulator", isUsingEmulator());
+ }
+
+ @Before
+ public void setUp() {
+ testHelper = env.getTestHelper();
+ dbAdminClient = testHelper.getClient().getDatabaseAdminClient();
+ databasesToDrop = new ArrayList<>();
+ }
+
+ @After
+ public void tearDown() {
+ for (Database database : databasesToDrop) {
+ final DatabaseId id = database.getId();
+ dbAdminClient.dropDatabase(id.getInstanceId().getInstance(), id.getDatabase());
+ }
+ }
+
+ @Test
+ public void returnsTheVersionRetentionPeriodSetThroughCreateDatabase() throws Exception {
+ final String instanceId = testHelper.getInstanceId().getInstance();
+ final String databaseId = testHelper.getUniqueDatabaseId();
+ final String extraStatement =
+ "ALTER DATABASE "
+ + databaseId
+ + " SET OPTIONS (version_retention_period = '"
+ + VERSION_RETENTION_PERIOD
+ + "')";
+
+ final Database database = createDatabase(instanceId, databaseId, extraStatement);
+
+ assertThat(database.getVersionRetentionPeriod()).isEqualTo(VERSION_RETENTION_PERIOD);
+ assertThat(database.getEarliestVersionTime()).isNotNull();
+ }
+
+ @Test
+ public void returnsTheVersionRetentionPeriodSetThroughGetDatabase() throws Exception {
+ final String instanceId = testHelper.getInstanceId().getInstance();
+ final String databaseId = testHelper.getUniqueDatabaseId();
+ final String extraStatement =
+ "ALTER DATABASE "
+ + databaseId
+ + " SET OPTIONS (version_retention_period = '"
+ + VERSION_RETENTION_PERIOD
+ + "')";
+
+ createDatabase(instanceId, databaseId, extraStatement);
+ final Database database = dbAdminClient.getDatabase(instanceId, databaseId);
+
+ assertThat(database.getVersionRetentionPeriod()).isEqualTo(VERSION_RETENTION_PERIOD);
+ assertThat(database.getEarliestVersionTime()).isNotNull();
+ }
+
+ @Test(expected = DatabaseNotFoundException.class)
+ public void returnsAnErrorWhenAnInvalidVersionRetentionPeriodIsGiven() {
+ final String instanceId = testHelper.getInstanceId().getInstance();
+ final String databaseId = testHelper.getUniqueDatabaseId();
+ final String extraStatement =
+ "ALTER DATABASE " + databaseId + " SET OPTIONS (version_retention_period = '0d')";
+
+ try {
+ createDatabase(instanceId, databaseId, extraStatement);
+ fail("Expected invalid argument error when setting invalid version retention period");
+ } catch (Exception e) {
+ SpannerException spannerException = (SpannerException) e.getCause();
+ assertThat(spannerException.getErrorCode()).isEqualTo(ErrorCode.INVALID_ARGUMENT);
+ }
+
+ // Expects a database not found exception
+ dbAdminClient.getDatabase(instanceId, databaseId);
+ }
+
+ private Database createDatabase(
+ final String instanceId, final String databaseId, final String extraStatement)
+ throws Exception {
+ final Database database =
+ dbAdminClient
+ .createDatabase(instanceId, databaseId, Collections.singletonList(extraStatement))
+ .get(OPERATION_TIMEOUT.toNanos(), TimeUnit.NANOSECONDS);
+ databasesToDrop.add(database);
+
+ return database;
+ }
+}
diff --git a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/it/ITPitrUpdateDatabaseTest.java b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/it/ITPitrUpdateDatabaseTest.java
new file mode 100644
index 0000000000..72a52b3907
--- /dev/null
+++ b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/it/ITPitrUpdateDatabaseTest.java
@@ -0,0 +1,207 @@
+/*
+ * Copyright 2020 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.
+ */
+
+package com.google.cloud.spanner.it;
+
+import static com.google.cloud.spanner.testing.EmulatorSpannerHelper.isUsingEmulator;
+import static com.google.common.truth.Truth.assertThat;
+import static org.junit.Assert.fail;
+import static org.junit.Assume.assumeFalse;
+
+import com.google.api.gax.longrunning.OperationFuture;
+import com.google.api.gax.paging.Page;
+import com.google.cloud.spanner.Database;
+import com.google.cloud.spanner.DatabaseAdminClient;
+import com.google.cloud.spanner.DatabaseClient;
+import com.google.cloud.spanner.DatabaseId;
+import com.google.cloud.spanner.ErrorCode;
+import com.google.cloud.spanner.IntegrationTestEnv;
+import com.google.cloud.spanner.ParallelIntegrationTest;
+import com.google.cloud.spanner.ResultSet;
+import com.google.cloud.spanner.SpannerException;
+import com.google.cloud.spanner.Statement;
+import com.google.cloud.spanner.testing.RemoteSpannerHelper;
+import com.google.spanner.admin.database.v1.UpdateDatabaseDdlMetadata;
+import java.util.Collections;
+import java.util.List;
+import java.util.concurrent.TimeUnit;
+import org.junit.AfterClass;
+import org.junit.BeforeClass;
+import org.junit.ClassRule;
+import org.junit.Test;
+import org.junit.experimental.categories.Category;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+import org.threeten.bp.Duration;
+
+@Category(ParallelIntegrationTest.class)
+@RunWith(JUnit4.class)
+public class ITPitrUpdateDatabaseTest {
+
+ private static final Duration OPERATION_TIMEOUT = Duration.ofMinutes(2);
+ private static final String VERSION_RETENTION_PERIOD = "7d";
+
+ @ClassRule public static IntegrationTestEnv env = new IntegrationTestEnv();
+ private static DatabaseAdminClient dbAdminClient;
+ private static DatabaseClient dbClient;
+ private static String instanceId;
+ private static String databaseId;
+ private static UpdateDatabaseDdlMetadata metadata;
+
+ @BeforeClass
+ public static void setUp() throws Exception {
+ assumeFalse("PITR-lite features are not supported by the emulator", isUsingEmulator());
+
+ final RemoteSpannerHelper testHelper = env.getTestHelper();
+ final String projectId = testHelper.getOptions().getProjectId();
+ instanceId = testHelper.getInstanceId().getInstance();
+ databaseId = testHelper.getUniqueDatabaseId();
+ dbAdminClient = testHelper.getClient().getDatabaseAdminClient();
+
+ createDatabase(dbAdminClient, instanceId, databaseId, Collections.emptyList());
+ metadata =
+ updateVersionRetentionPeriod(
+ dbAdminClient, instanceId, databaseId, VERSION_RETENTION_PERIOD);
+
+ dbClient =
+ testHelper.getClient().getDatabaseClient(DatabaseId.of(projectId, instanceId, databaseId));
+ }
+
+ @AfterClass
+ public static void tearDown() {
+ if (!isUsingEmulator()) {
+ dbAdminClient.dropDatabase(instanceId, databaseId);
+ }
+ }
+
+ @Test
+ public void checksThatTheOperationWasNotThrottled() {
+ assertThat(metadata.getThrottled()).isFalse();
+ }
+
+ @Test
+ public void returnsTheVersionRetentionPeriodSetThroughGetDatabase() {
+ final Database database = dbAdminClient.getDatabase(instanceId, databaseId);
+
+ assertThat(database.getVersionRetentionPeriod()).isEqualTo(VERSION_RETENTION_PERIOD);
+ assertThat(database.getEarliestVersionTime()).isNotNull();
+ }
+
+ @Test
+ public void returnsTheVersionRetentionPeriodSetThroughListDatabases() {
+ final Page page = dbAdminClient.listDatabases(instanceId);
+
+ for (Database database : page.iterateAll()) {
+ if (!database.getId().getDatabase().equals(databaseId)) {
+ continue;
+ }
+ assertThat(database.getVersionRetentionPeriod()).isEqualTo(VERSION_RETENTION_PERIOD);
+ assertThat(database.getEarliestVersionTime()).isNotNull();
+ }
+ }
+
+ @Test
+ public void returnsTheVersionRetentionPeriodSetThroughGetDatabaseDdl() {
+ final List ddls = dbAdminClient.getDatabaseDdl(instanceId, databaseId);
+
+ boolean hasVersionRetentionPeriodStatement = false;
+ for (String ddl : ddls) {
+ hasVersionRetentionPeriodStatement =
+ ddl.contains("version_retention_period = '" + VERSION_RETENTION_PERIOD + "'");
+ if (hasVersionRetentionPeriodStatement) {
+ break;
+ }
+ }
+ assertThat(hasVersionRetentionPeriodStatement).isTrue();
+ }
+
+ @Test
+ public void returnsTheVersionRetentionPeriodSetThroughInformationSchema() {
+ try (final ResultSet rs =
+ dbClient
+ .singleUse()
+ .executeQuery(
+ Statement.of(
+ "SELECT OPTION_VALUE AS version_retention_period "
+ + "FROM INFORMATION_SCHEMA.DATABASE_OPTIONS "
+ + "WHERE SCHEMA_NAME = '' AND OPTION_NAME = 'version_retention_period'"))) {
+
+ String versionRetentionPeriod = null;
+ while (rs.next()) {
+ versionRetentionPeriod = rs.getString("version_retention_period");
+ }
+
+ assertThat(versionRetentionPeriod).isEqualTo(VERSION_RETENTION_PERIOD);
+ }
+ }
+
+ @Test
+ public void returnsAnErrorWhenAnInvalidRetentionPeriodIsGiven() {
+ try {
+ dbAdminClient
+ .updateDatabaseDdl(
+ instanceId,
+ databaseId,
+ Collections.singletonList(
+ "ALTER DATABASE "
+ + databaseId
+ + " SET OPTIONS (version_retention_period = '0d')"),
+ "op_invalid_retention_period_" + databaseId)
+ .get(OPERATION_TIMEOUT.toNanos(), TimeUnit.NANOSECONDS);
+ fail("Expected invalid argument error when setting invalid version retention period");
+ } catch (Exception e) {
+ SpannerException spannerException = (SpannerException) e.getCause();
+ assertThat(spannerException.getErrorCode()).isEqualTo(ErrorCode.INVALID_ARGUMENT);
+ }
+
+ final Database database = dbAdminClient.getDatabase(instanceId, databaseId);
+
+ assertThat(database.getVersionRetentionPeriod()).isEqualTo(VERSION_RETENTION_PERIOD);
+ assertThat(database.getEarliestVersionTime()).isNotNull();
+ }
+
+ private static Database createDatabase(
+ final DatabaseAdminClient dbAdminClient,
+ final String instanceId,
+ final String databaseId,
+ final Iterable extraStatements)
+ throws Exception {
+ return dbAdminClient
+ .createDatabase(instanceId, databaseId, extraStatements)
+ .get(OPERATION_TIMEOUT.toNanos(), TimeUnit.NANOSECONDS);
+ }
+
+ private static UpdateDatabaseDdlMetadata updateVersionRetentionPeriod(
+ final DatabaseAdminClient dbAdminClient,
+ final String instanceId,
+ final String databaseId,
+ final String versionRetentionPeriod)
+ throws Exception {
+ final OperationFuture op =
+ dbAdminClient.updateDatabaseDdl(
+ instanceId,
+ databaseId,
+ Collections.singletonList(
+ "ALTER DATABASE "
+ + databaseId
+ + " SET OPTIONS ( version_retention_period = '"
+ + versionRetentionPeriod
+ + "' )"),
+ "updateddl_version_retention_period");
+ op.get(OPERATION_TIMEOUT.toNanos(), TimeUnit.NANOSECONDS);
+ return op.getMetadata().get(OPERATION_TIMEOUT.toNanos(), TimeUnit.NANOSECONDS);
+ }
+}