Skip to content

Commit 5d243ad

Browse files
committed
Merge pull request #549 from ajkannan/set-cursor
Populate cursorAfter in datastore v1beta2
2 parents cdad97a + 461f375 commit 5d243ad

File tree

4 files changed

+117
-6
lines changed

4 files changed

+117
-6
lines changed

gcloud-java-datastore/src/main/java/com/google/gcloud/datastore/QueryResults.java

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -22,8 +22,9 @@
2222
* The result of a Google Cloud Datastore query submission.
2323
* When the result is not typed it is possible to cast it to its appropriate type according to
2424
* the {@link #resultClass} value.
25-
* Results are loaded lazily; therefore it is possible to get a {@code DatastoreException}
26-
* upon {@link Iterator#hasNext hasNext} or {@link Iterator#next next} calls.
25+
* Results are loaded lazily in batches, where batch size is set by Cloud Datastore. As a result, it
26+
* is possible to get a {@code DatastoreException} upon {@link Iterator#hasNext hasNext} or
27+
* {@link Iterator#next next} calls.
2728
*
2829
* @param <V> the type of the results value.
2930
*/
@@ -35,8 +36,8 @@ public interface QueryResults<V> extends Iterator<V> {
3536
Class<?> resultClass();
3637

3738
/**
38-
* Returns the Cursor for point after the value returned in the last {@link #next} call.
39-
* Not currently implemented (depends on v1beta3).
39+
* Returns the Cursor for the point after the value returned in the last {@link #next} call.
40+
* Currently, {@code cursorAfter} returns null in all cases but the last result.
4041
*/
4142
Cursor cursorAfter();
4243
}

gcloud-java-datastore/src/main/java/com/google/gcloud/datastore/QueryResultsImpl.java

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
import com.google.common.base.Preconditions;
2222
import com.google.common.collect.AbstractIterator;
2323
import com.google.gcloud.datastore.Query.ResultType;
24+
import com.google.protobuf.ByteString;
2425

2526
import java.util.Iterator;
2627
import java.util.Objects;
@@ -36,7 +37,7 @@ class QueryResultsImpl<T> extends AbstractIterator<T> implements QueryResults<T>
3637
private DatastoreV1.QueryResultBatch queryResultBatchPb;
3738
private boolean lastBatch;
3839
private Iterator<DatastoreV1.EntityResult> entityResultPbIter;
39-
//private ByteString cursor; // only available in v1beta3
40+
private ByteString cursor; // only available in v1beta3
4041

4142

4243
QueryResultsImpl(DatastoreImpl datastore, DatastoreV1.ReadOptions readOptionsPb,
@@ -83,6 +84,7 @@ protected T computeNext() {
8384
sendRequest();
8485
}
8586
if (!entityResultPbIter.hasNext()) {
87+
cursor = queryResultBatchPb.getEndCursor();
8688
return endOfData();
8789
}
8890
DatastoreV1.EntityResult entityResultPb = entityResultPbIter.next();
@@ -99,7 +101,7 @@ public Class<?> resultClass() {
99101

100102
@Override
101103
public Cursor cursorAfter() {
104+
return cursor == null ? null : new Cursor(cursor);
102105
//return new Cursor(cursor); // only available in v1beta3
103-
return null;
104106
}
105107
}

gcloud-java-datastore/src/main/java/com/google/gcloud/datastore/StructuredQuery.java

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -633,6 +633,20 @@ protected static class BaseBuilder<V, B extends BaseBuilder<V, B>> {
633633
this.resultType = resultType;
634634
}
635635

636+
BaseBuilder(StructuredQuery<V> query) {
637+
resultType = query.type();
638+
namespace = query.namespace();
639+
kind = query.kind;
640+
projection.addAll(query.projection);
641+
filter = query.filter;
642+
groupBy.addAll(query.groupBy);
643+
orderBy.addAll(query.orderBy);
644+
startCursor = query.startCursor;
645+
endCursor = query.endCursor;
646+
offset = query.offset;
647+
limit = query.limit;
648+
}
649+
636650
@SuppressWarnings("unchecked")
637651
B self() {
638652
return (B) this;
@@ -773,6 +787,10 @@ static final class Builder<V> extends BaseBuilder<V, Builder<V>> {
773787
Builder(ResultType<V> resultType) {
774788
super(resultType);
775789
}
790+
791+
Builder(StructuredQuery<V> query) {
792+
super(query);
793+
}
776794
}
777795

778796
/**
@@ -953,6 +971,10 @@ public Integer limit() {
953971
return limit;
954972
}
955973

974+
public Builder<V> toBuilder() {
975+
return new Builder<>(this);
976+
}
977+
956978
@Override
957979
void populatePb(DatastoreV1.RunQueryRequest.Builder requestPb) {
958980
requestPb.setQuery(toPb());

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

Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,9 @@
2727

2828
import com.google.api.services.datastore.DatastoreV1;
2929
import com.google.api.services.datastore.DatastoreV1.EntityResult;
30+
import com.google.api.services.datastore.DatastoreV1.QueryResultBatch;
31+
import com.google.api.services.datastore.DatastoreV1.RunQueryRequest;
32+
import com.google.api.services.datastore.DatastoreV1.RunQueryResponse;
3033
import com.google.common.collect.Iterators;
3134
import com.google.gcloud.RetryParams;
3235
import com.google.gcloud.datastore.Query.ResultType;
@@ -462,6 +465,89 @@ public void testRunStructuredQuery() {
462465
assertFalse(results4.hasNext());
463466
}
464467

468+
@Test
469+
public void testQueryPaginationWithLimit() throws DatastoreRpcException {
470+
DatastoreRpcFactory rpcFactoryMock = EasyMock.createStrictMock(DatastoreRpcFactory.class);
471+
DatastoreRpc rpcMock = EasyMock.createStrictMock(DatastoreRpc.class);
472+
EasyMock.expect(rpcFactoryMock.create(EasyMock.anyObject(DatastoreOptions.class)))
473+
.andReturn(rpcMock);
474+
List<RunQueryResponse> responses = buildResponsesForQueryPaginationWithLimit();
475+
for (int i = 0; i < responses.size(); i++) {
476+
EasyMock.expect(rpcMock.runQuery(EasyMock.anyObject(RunQueryRequest.class)))
477+
.andReturn(responses.get(i));
478+
}
479+
EasyMock.replay(rpcFactoryMock, rpcMock);
480+
Datastore mockDatastore = options.toBuilder()
481+
.retryParams(RetryParams.defaultInstance())
482+
.serviceRpcFactory(rpcFactoryMock)
483+
.build()
484+
.service();
485+
int limit = 2;
486+
int totalCount = 0;
487+
StructuredQuery<Entity> query = Query.entityQueryBuilder().limit(limit).build();
488+
while (true) {
489+
QueryResults<Entity> results = mockDatastore.run(query);
490+
int resultCount = 0;
491+
while (results.hasNext()) {
492+
results.next();
493+
resultCount++;
494+
totalCount++;
495+
}
496+
if (resultCount < limit) {
497+
break;
498+
}
499+
query = query.toBuilder().startCursor(results.cursorAfter()).build();
500+
}
501+
assertEquals(totalCount, 5);
502+
EasyMock.verify(rpcFactoryMock, rpcMock);
503+
}
504+
505+
private List<RunQueryResponse> buildResponsesForQueryPaginationWithLimit() {
506+
Entity entity4 = Entity.builder(KEY4).set("value", StringValue.of("value")).build();
507+
Entity entity5 = Entity.builder(KEY5).set("value", "value").build();
508+
datastore.add(ENTITY3, entity4, entity5);
509+
List<RunQueryResponse> responses = new ArrayList<>();
510+
Query<Entity> query = Query.entityQueryBuilder().build();
511+
RunQueryRequest.Builder requestPb = RunQueryRequest.newBuilder();
512+
query.populatePb(requestPb);
513+
QueryResultBatch queryResultBatchPb = RunQueryResponse.newBuilder()
514+
.mergeFrom(((DatastoreImpl) datastore).runQuery(requestPb.build()))
515+
.getBatch();
516+
QueryResultBatch queryResultBatchPb1 = QueryResultBatch.newBuilder()
517+
.mergeFrom(queryResultBatchPb)
518+
.setMoreResults(QueryResultBatch.MoreResultsType.NOT_FINISHED)
519+
.clearEntityResult()
520+
.addAllEntityResult(queryResultBatchPb.getEntityResultList().subList(0, 1))
521+
.setEndCursor(queryResultBatchPb.getEntityResultList().get(0).getCursor())
522+
.build();
523+
responses.add(RunQueryResponse.newBuilder().setBatch(queryResultBatchPb1).build());
524+
QueryResultBatch queryResultBatchPb2 = QueryResultBatch.newBuilder()
525+
.mergeFrom(queryResultBatchPb)
526+
.setMoreResults(QueryResultBatch.MoreResultsType.MORE_RESULTS_AFTER_LIMIT)
527+
.clearEntityResult()
528+
.addAllEntityResult(queryResultBatchPb.getEntityResultList().subList(1, 2))
529+
.setEndCursor(queryResultBatchPb.getEntityResultList().get(1).getCursor())
530+
.build();
531+
responses.add(RunQueryResponse.newBuilder().setBatch(queryResultBatchPb2).build());
532+
QueryResultBatch queryResultBatchPb3 = QueryResultBatch.newBuilder()
533+
.mergeFrom(queryResultBatchPb)
534+
.setMoreResults(QueryResultBatch.MoreResultsType.MORE_RESULTS_AFTER_LIMIT)
535+
.clearEntityResult()
536+
.addAllEntityResult(queryResultBatchPb.getEntityResultList().subList(2, 4))
537+
.setEndCursor(queryResultBatchPb.getEntityResultList().get(3).getCursor())
538+
.build();
539+
responses.add(RunQueryResponse.newBuilder().setBatch(queryResultBatchPb3).build());
540+
QueryResultBatch queryResultBatchPb4 = QueryResultBatch.newBuilder()
541+
.mergeFrom(queryResultBatchPb)
542+
.setMoreResults(QueryResultBatch.MoreResultsType.NO_MORE_RESULTS)
543+
.clearEntityResult()
544+
.addAllEntityResult(queryResultBatchPb.getEntityResultList().subList(4, 5))
545+
.setEndCursor(queryResultBatchPb.getEntityResultList().get(4).getCursor())
546+
.build();
547+
responses.add(RunQueryResponse.newBuilder().setBatch(queryResultBatchPb4).build());
548+
return responses;
549+
}
550+
465551
@Test
466552
public void testAllocateId() {
467553
KeyFactory keyFactory = datastore.newKeyFactory().kind(KIND1);

0 commit comments

Comments
 (0)