Skip to content
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
import com.google.cloud.RetryHelper.RetryHelperException;
import com.google.cloud.ServiceOptions;
import com.google.cloud.datastore.ReadOption.EventualConsistency;
import com.google.cloud.datastore.ReadOption.ReadTime;
import com.google.cloud.datastore.spi.v1.DatastoreRpc;
import com.google.common.base.MoreObjects;
import com.google.common.base.Preconditions;
Expand Down Expand Up @@ -338,12 +339,29 @@ public Iterator<Entity> get(Iterable<Key> keys, ReadOption... options) {

private static com.google.datastore.v1.ReadOptions toReadOptionsPb(ReadOption... options) {
com.google.datastore.v1.ReadOptions readOptionsPb = null;
if (options != null
&& ReadOption.asImmutableMap(options).containsKey(EventualConsistency.class)) {
readOptionsPb =
com.google.datastore.v1.ReadOptions.newBuilder()
.setReadConsistency(ReadConsistency.EVENTUAL)
.build();
if (options != null) {
Map<Class<? extends ReadOption>, ReadOption> optionsByType =
ReadOption.asImmutableMap(options);

if (optionsByType.containsKey(EventualConsistency.class)
&& optionsByType.containsKey(ReadTime.class)) {
throw DatastoreException.throwInvalidRequest(
"Can not use eventual consistency read with read time.");
}

if (optionsByType.containsKey(EventualConsistency.class)) {
readOptionsPb =
com.google.datastore.v1.ReadOptions.newBuilder()
.setReadConsistency(ReadConsistency.EVENTUAL)
.build();
}

if (optionsByType.containsKey(ReadTime.class)) {
readOptionsPb =
com.google.datastore.v1.ReadOptions.newBuilder()
.setReadTime(((ReadTime) optionsByType.get(ReadTime.class)).time().toProto())
.build();
}
}
return readOptionsPb;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@

package com.google.cloud.datastore;

import com.google.cloud.Timestamp;
import com.google.common.collect.ImmutableMap;
import java.io.Serializable;
import java.util.Map;
Expand Down Expand Up @@ -47,6 +48,25 @@ public boolean isEventual() {
}
}

/**
* Reads entities as they were at the given time. This may not be older than 270 seconds.
* This value is only supported for Cloud Firestore in Datastore mode.
*/
public static final class ReadTime extends ReadOption {

private static final long serialVersionUID = -6780321449114616067L;

private final Timestamp time;

private ReadTime(Timestamp time) {
this.time = time;
}

public Timestamp time() {
return time;
}
}

private ReadOption() {}

/**
Expand All @@ -57,6 +77,14 @@ public static EventualConsistency eventualConsistency() {
return new EventualConsistency(true);
}

/**
* Returns a {@code ReadOption} that specifies read time, allowing Datastore to return results
* from lookups and queries at a particular timestamp.
*/
public static ReadTime readTime(Timestamp time) {
return new ReadTime(time);
}

static Map<Class<? extends ReadOption>, ReadOption> asImmutableMap(ReadOption... options) {
ImmutableMap.Builder<Class<? extends ReadOption>, ReadOption> builder = ImmutableMap.builder();
for (ReadOption option : options) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -789,6 +789,27 @@ public void testEventualConsistencyQuery() {
EasyMock.verify(rpcFactoryMock, rpcMock);
}

@Test
public void testReadTimeQuery() {
Timestamp timestamp = Timestamp.now();
ReadOptions readOption =
ReadOptions.newBuilder().setReadTime(timestamp.toProto()).build();
com.google.datastore.v1.GqlQuery query =
com.google.datastore.v1.GqlQuery.newBuilder().setQueryString("FROM * SELECT *").build();
RunQueryRequest.Builder expectedRequest =
RunQueryRequest.newBuilder()
.setReadOptions(readOption)
.setGqlQuery(query)
.setPartitionId(PartitionId.newBuilder().setProjectId(PROJECT_ID).build());
EasyMock.expect(rpcMock.runQuery(expectedRequest.build()))
.andReturn(RunQueryResponse.newBuilder().build());
EasyMock.replay(rpcFactoryMock, rpcMock);
Datastore datastore = rpcMockOptions.getService();
datastore.run(
Query.newGqlQueryBuilder("FROM * SELECT *").build(), ReadOption.readTime(timestamp));
EasyMock.verify(rpcFactoryMock, rpcMock);
}

@Test
public void testToUrlSafe() {
byte[][] invalidUtf8 =
Expand Down Expand Up @@ -921,6 +942,33 @@ public void testLookupEventualConsistency() {
EasyMock.verify(rpcFactoryMock, rpcMock);
}

@Test
public void testLookupReadTime() {
Timestamp timestamp = Timestamp.now();
ReadOptions readOption =
ReadOptions.newBuilder().setReadTime(timestamp.toProto()).build();
com.google.datastore.v1.Key key =
com.google.datastore.v1.Key.newBuilder()
.setPartitionId(PartitionId.newBuilder().setProjectId(PROJECT_ID).build())
.addPath(
com.google.datastore.v1.Key.PathElement.newBuilder()
.setKind("kind1")
.setName("name")
.build())
.build();
LookupRequest lookupRequest =
LookupRequest.newBuilder().setReadOptions(readOption).addKeys(key).build();
EasyMock.expect(rpcMock.lookup(lookupRequest))
.andReturn(LookupResponse.newBuilder().build())
.times(3);
EasyMock.replay(rpcFactoryMock, rpcMock);
com.google.cloud.datastore.Datastore datastore = rpcMockOptions.getService();
datastore.get(KEY1, com.google.cloud.datastore.ReadOption.readTime(timestamp));
datastore.get(ImmutableList.of(KEY1), com.google.cloud.datastore.ReadOption.readTime(timestamp));
datastore.fetch(ImmutableList.of(KEY1), com.google.cloud.datastore.ReadOption.readTime(timestamp));
EasyMock.verify(rpcFactoryMock, rpcMock);
}

@Test
public void testGetArrayNoDeferredResults() {
datastore.put(ENTITY3);
Expand Down