Skip to content

Commit 06bb08f

Browse files
authored
feat: add ReadOption.ReadTime to support timestamp reads. (#712)
Thank you for opening a Pull Request! Before submitting your PR, there are a few things you can do to make sure it goes smoothly: - [ ] Make sure to open an issue as a [bug/issue](https://github.com/googleapis/java-datastore/issues/new/choose) before writing your code! That way we can discuss the change, evaluate designs, and agree on the general idea - [ ] Ensure the tests and linter pass - [ ] Code coverage does not decrease (if any source code was changed) - [ ] Appropriate docs were updated (if necessary) Fixes #<issue_number_goes_here> ☕️ If you write sample code, please follow the [samples format]( https://github.com/GoogleCloudPlatform/java-docs-samples/blob/main/SAMPLE_FORMAT.md).
1 parent a057ecb commit 06bb08f

File tree

4 files changed

+173
-6
lines changed

4 files changed

+173
-6
lines changed

google-cloud-datastore/src/main/java/com/google/cloud/datastore/DatastoreImpl.java

+24-6
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
import com.google.cloud.RetryHelper.RetryHelperException;
2424
import com.google.cloud.ServiceOptions;
2525
import com.google.cloud.datastore.ReadOption.EventualConsistency;
26+
import com.google.cloud.datastore.ReadOption.ReadTime;
2627
import com.google.cloud.datastore.spi.v1.DatastoreRpc;
2728
import com.google.common.base.MoreObjects;
2829
import com.google.common.base.Preconditions;
@@ -338,12 +339,29 @@ public Iterator<Entity> get(Iterable<Key> keys, ReadOption... options) {
338339

339340
private static com.google.datastore.v1.ReadOptions toReadOptionsPb(ReadOption... options) {
340341
com.google.datastore.v1.ReadOptions readOptionsPb = null;
341-
if (options != null
342-
&& ReadOption.asImmutableMap(options).containsKey(EventualConsistency.class)) {
343-
readOptionsPb =
344-
com.google.datastore.v1.ReadOptions.newBuilder()
345-
.setReadConsistency(ReadConsistency.EVENTUAL)
346-
.build();
342+
if (options != null) {
343+
Map<Class<? extends ReadOption>, ReadOption> optionsByType =
344+
ReadOption.asImmutableMap(options);
345+
346+
if (optionsByType.containsKey(EventualConsistency.class)
347+
&& optionsByType.containsKey(ReadTime.class)) {
348+
throw DatastoreException.throwInvalidRequest(
349+
"Can not use eventual consistency read with read time.");
350+
}
351+
352+
if (optionsByType.containsKey(EventualConsistency.class)) {
353+
readOptionsPb =
354+
com.google.datastore.v1.ReadOptions.newBuilder()
355+
.setReadConsistency(ReadConsistency.EVENTUAL)
356+
.build();
357+
}
358+
359+
if (optionsByType.containsKey(ReadTime.class)) {
360+
readOptionsPb =
361+
com.google.datastore.v1.ReadOptions.newBuilder()
362+
.setReadTime(((ReadTime) optionsByType.get(ReadTime.class)).time().toProto())
363+
.build();
364+
}
347365
}
348366
return readOptionsPb;
349367
}

google-cloud-datastore/src/main/java/com/google/cloud/datastore/ReadOption.java

+31
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,8 @@
1616

1717
package com.google.cloud.datastore;
1818

19+
import com.google.api.core.BetaApi;
20+
import com.google.cloud.Timestamp;
1921
import com.google.common.collect.ImmutableMap;
2022
import java.io.Serializable;
2123
import java.util.Map;
@@ -47,6 +49,25 @@ public boolean isEventual() {
4749
}
4850
}
4951

52+
/**
53+
* Reads entities as they were at the given time. This may not be older than 270 seconds. This
54+
* value is only supported for Cloud Firestore in Datastore mode.
55+
*/
56+
public static final class ReadTime extends ReadOption {
57+
58+
private static final long serialVersionUID = -6780321449114616067L;
59+
60+
private final Timestamp time;
61+
62+
private ReadTime(Timestamp time) {
63+
this.time = time;
64+
}
65+
66+
public Timestamp time() {
67+
return time;
68+
}
69+
}
70+
5071
private ReadOption() {}
5172

5273
/**
@@ -57,6 +78,16 @@ public static EventualConsistency eventualConsistency() {
5778
return new EventualConsistency(true);
5879
}
5980

81+
/**
82+
* Returns a {@code ReadOption} that specifies read time, allowing Datastore to return results
83+
* from lookups and queries at a particular timestamp. This feature is currently in private
84+
* preview.
85+
*/
86+
@BetaApi
87+
public static ReadTime readTime(Timestamp time) {
88+
return new ReadTime(time);
89+
}
90+
6091
static Map<Class<? extends ReadOption>, ReadOption> asImmutableMap(ReadOption... options) {
6192
ImmutableMap.Builder<Class<? extends ReadOption>, ReadOption> builder = ImmutableMap.builder();
6293
for (ReadOption option : options) {

google-cloud-datastore/src/test/java/com/google/cloud/datastore/DatastoreTest.java

+48
Original file line numberDiff line numberDiff line change
@@ -789,6 +789,26 @@ public void testEventualConsistencyQuery() {
789789
EasyMock.verify(rpcFactoryMock, rpcMock);
790790
}
791791

792+
@Test
793+
public void testReadTimeQuery() {
794+
Timestamp timestamp = Timestamp.now();
795+
ReadOptions readOption = ReadOptions.newBuilder().setReadTime(timestamp.toProto()).build();
796+
com.google.datastore.v1.GqlQuery query =
797+
com.google.datastore.v1.GqlQuery.newBuilder().setQueryString("FROM * SELECT *").build();
798+
RunQueryRequest.Builder expectedRequest =
799+
RunQueryRequest.newBuilder()
800+
.setReadOptions(readOption)
801+
.setGqlQuery(query)
802+
.setPartitionId(PartitionId.newBuilder().setProjectId(PROJECT_ID).build());
803+
EasyMock.expect(rpcMock.runQuery(expectedRequest.build()))
804+
.andReturn(RunQueryResponse.newBuilder().build());
805+
EasyMock.replay(rpcFactoryMock, rpcMock);
806+
Datastore datastore = rpcMockOptions.getService();
807+
datastore.run(
808+
Query.newGqlQueryBuilder("FROM * SELECT *").build(), ReadOption.readTime(timestamp));
809+
EasyMock.verify(rpcFactoryMock, rpcMock);
810+
}
811+
792812
@Test
793813
public void testToUrlSafe() {
794814
byte[][] invalidUtf8 =
@@ -921,6 +941,34 @@ public void testLookupEventualConsistency() {
921941
EasyMock.verify(rpcFactoryMock, rpcMock);
922942
}
923943

944+
@Test
945+
public void testLookupReadTime() {
946+
Timestamp timestamp = Timestamp.now();
947+
ReadOptions readOption = ReadOptions.newBuilder().setReadTime(timestamp.toProto()).build();
948+
com.google.datastore.v1.Key key =
949+
com.google.datastore.v1.Key.newBuilder()
950+
.setPartitionId(PartitionId.newBuilder().setProjectId(PROJECT_ID).build())
951+
.addPath(
952+
com.google.datastore.v1.Key.PathElement.newBuilder()
953+
.setKind("kind1")
954+
.setName("name")
955+
.build())
956+
.build();
957+
LookupRequest lookupRequest =
958+
LookupRequest.newBuilder().setReadOptions(readOption).addKeys(key).build();
959+
EasyMock.expect(rpcMock.lookup(lookupRequest))
960+
.andReturn(LookupResponse.newBuilder().build())
961+
.times(3);
962+
EasyMock.replay(rpcFactoryMock, rpcMock);
963+
com.google.cloud.datastore.Datastore datastore = rpcMockOptions.getService();
964+
datastore.get(KEY1, com.google.cloud.datastore.ReadOption.readTime(timestamp));
965+
datastore.get(
966+
ImmutableList.of(KEY1), com.google.cloud.datastore.ReadOption.readTime(timestamp));
967+
datastore.fetch(
968+
ImmutableList.of(KEY1), com.google.cloud.datastore.ReadOption.readTime(timestamp));
969+
EasyMock.verify(rpcFactoryMock, rpcMock);
970+
}
971+
924972
@Test
925973
public void testGetArrayNoDeferredResults() {
926974
datastore.put(ENTITY3);

google-cloud-datastore/src/test/java/com/google/cloud/datastore/it/ITDatastoreTest.java

+70
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@
5050
import com.google.cloud.datastore.Query;
5151
import com.google.cloud.datastore.Query.ResultType;
5252
import com.google.cloud.datastore.QueryResults;
53+
import com.google.cloud.datastore.ReadOption;
5354
import com.google.cloud.datastore.StringValue;
5455
import com.google.cloud.datastore.StructuredQuery;
5556
import com.google.cloud.datastore.StructuredQuery.OrderBy;
@@ -647,6 +648,31 @@ public void testGet() {
647648
assertFalse(entity.contains("bla"));
648649
}
649650

651+
@Test
652+
public void testGetWithReadTime() throws InterruptedException {
653+
Key key = Key.newBuilder(PROJECT_ID, "new_kind", "name").setNamespace(NAMESPACE).build();
654+
655+
try {
656+
DATASTORE.put(Entity.newBuilder(key).set("str", "old_str_value").build());
657+
658+
Thread.sleep(1000);
659+
Timestamp now = Timestamp.now();
660+
Thread.sleep(1000);
661+
662+
DATASTORE.put(Entity.newBuilder(key).set("str", "new_str_value").build());
663+
664+
Entity entity = DATASTORE.get(key);
665+
StringValue value1 = entity.getValue("str");
666+
assertEquals(StringValue.of("new_str_value"), value1);
667+
668+
entity = DATASTORE.get(key, ReadOption.readTime(now));
669+
value1 = entity.getValue("str");
670+
assertEquals(StringValue.of("old_str_value"), value1);
671+
} finally {
672+
DATASTORE.delete(key);
673+
}
674+
}
675+
650676
@Test
651677
public void testGetArrayNoDeferredResults() {
652678
DATASTORE.put(ENTITY3);
@@ -920,4 +946,48 @@ public void testQueryWithStartCursor() {
920946
assertEquals(cursor2, cursor1);
921947
DATASTORE.delete(entity1.getKey(), entity2.getKey(), entity3.getKey());
922948
}
949+
950+
@Test
951+
public void testQueryWithReadTime() throws InterruptedException {
952+
Entity entity1 =
953+
Entity.newBuilder(
954+
Key.newBuilder(PROJECT_ID, "new_kind", "name-01").setNamespace(NAMESPACE).build())
955+
.build();
956+
Entity entity2 =
957+
Entity.newBuilder(
958+
Key.newBuilder(PROJECT_ID, "new_kind", "name-02").setNamespace(NAMESPACE).build())
959+
.build();
960+
Entity entity3 =
961+
Entity.newBuilder(
962+
Key.newBuilder(PROJECT_ID, "new_kind", "name-03").setNamespace(NAMESPACE).build())
963+
.build();
964+
965+
DATASTORE.put(entity1, entity2);
966+
Thread.sleep(1000);
967+
Timestamp now = Timestamp.now();
968+
Thread.sleep(1000);
969+
DATASTORE.put(entity3);
970+
971+
try {
972+
Query<Entity> query = Query.newEntityQueryBuilder().setKind("new_kind").build();
973+
974+
QueryResults<Entity> withoutReadTime = DATASTORE.run(query);
975+
assertTrue(withoutReadTime.hasNext());
976+
assertEquals(entity1, withoutReadTime.next());
977+
assertTrue(withoutReadTime.hasNext());
978+
assertEquals(entity2, withoutReadTime.next());
979+
assertTrue(withoutReadTime.hasNext());
980+
assertEquals(entity3, withoutReadTime.next());
981+
assertFalse(withoutReadTime.hasNext());
982+
983+
QueryResults<Entity> withReadTime = DATASTORE.run(query, ReadOption.readTime(now));
984+
assertTrue(withReadTime.hasNext());
985+
assertEquals(entity1, withReadTime.next());
986+
assertTrue(withReadTime.hasNext());
987+
assertEquals(entity2, withReadTime.next());
988+
assertFalse(withReadTime.hasNext());
989+
} finally {
990+
DATASTORE.delete(entity1.getKey(), entity2.getKey(), entity3.getKey());
991+
}
992+
}
923993
}

0 commit comments

Comments
 (0)