From c104615210271977a48205a8d8e6acd69acc5fb6 Mon Sep 17 00:00:00 2001 From: Sebastian Schmidt Date: Fri, 27 Mar 2020 11:19:06 -0700 Subject: [PATCH] feat: add Query.limitToLast() (#151) --- .../com/google/cloud/firestore/Query.java | 106 ++++++++++++-- .../com/google/cloud/firestore/QueryTest.java | 91 ++++++++++++ .../cloud/firestore/it/ITQueryWatchTest.java | 132 ++++++++++-------- .../cloud/firestore/it/ITSystemTest.java | 63 +++++---- 4 files changed, 298 insertions(+), 94 deletions(-) diff --git a/google-cloud-firestore/src/main/java/com/google/cloud/firestore/Query.java b/google-cloud-firestore/src/main/java/com/google/cloud/firestore/Query.java index ccc330b80..aa5ea84e5 100644 --- a/google-cloud-firestore/src/main/java/com/google/cloud/firestore/Query.java +++ b/google-cloud-firestore/src/main/java/com/google/cloud/firestore/Query.java @@ -16,6 +16,7 @@ package com.google.cloud.firestore; +import static com.google.common.collect.Lists.reverse; import static com.google.firestore.v1.StructuredQuery.FieldFilter.Operator.ARRAY_CONTAINS; import static com.google.firestore.v1.StructuredQuery.FieldFilter.Operator.ARRAY_CONTAINS_ANY; import static com.google.firestore.v1.StructuredQuery.FieldFilter.Operator.EQUAL; @@ -182,6 +183,12 @@ Order toProto() { } } + /** Denotes whether a provided limit is applied to the beginning or the end of the result set. */ + enum LimitType { + First, + Last + } + /** Options that define a Firestore Query. */ @AutoValue abstract static class QueryOptions { @@ -194,6 +201,8 @@ abstract static class QueryOptions { abstract @Nullable Integer getLimit(); + abstract LimitType getLimitType(); + abstract @Nullable Integer getOffset(); abstract @Nullable Cursor getStartCursor(); @@ -209,6 +218,7 @@ abstract static class QueryOptions { static Builder builder() { return new AutoValue_Query_QueryOptions.Builder() .setAllDescendants(false) + .setLimitType(LimitType.First) .setFieldOrders(ImmutableList.of()) .setFieldFilters(ImmutableList.of()) .setFieldProjections(ImmutableList.of()); @@ -226,6 +236,8 @@ abstract static class Builder { abstract Builder setLimit(Integer value); + abstract Builder setLimitType(LimitType value); + abstract Builder setOffset(Integer value); abstract Builder setStartCursor(@Nullable Cursor value); @@ -764,15 +776,33 @@ public Query orderBy(@Nonnull FieldPath fieldPath, @Nonnull Direction direction) } /** - * Creates and returns a new Query that's additionally limited to only return up to the specified - * number of documents. + * Creates and returns a new Query that only returns the first matching documents. * * @param limit The maximum number of items to return. * @return The created Query. */ @Nonnull public Query limit(int limit) { - return new Query(firestore, options.toBuilder().setLimit(limit).build()); + return new Query( + firestore, options.toBuilder().setLimit(limit).setLimitType(LimitType.First).build()); + } + + /** + * Creates and returns a new Query that only returns the last matching documents. + * + *

You must specify at least one orderBy clause for limitToLast queries. Otherwise, an {@link + * java.lang.IllegalStateException} is thrown during execution. + * + *

Results for limitToLast() queries are only available once all documents are received. Hence, + * limitToLast() queries cannot be streamed via the {@link #stream(ApiStreamObserver)} API. + * + * @param limit the maximum number of items to return + * @return the created Query + */ + @Nonnull + public Query limitToLast(int limit) { + return new Query( + firestore, options.toBuilder().setLimit(limit).setLimitType(LimitType.Last).build()); } /** @@ -1004,8 +1034,25 @@ StructuredQuery.Builder buildQuery() { if (!options.getFieldOrders().isEmpty()) { for (FieldOrder order : options.getFieldOrders()) { - structuredQuery.addOrderBy(order.toProto()); + switch (options.getLimitType()) { + case First: + structuredQuery.addOrderBy(order.toProto()); + break; + case Last: + // Flip the orderBy directions since we want the last results + order = + new FieldOrder( + order.fieldPath, + order.direction.equals(Direction.ASCENDING) + ? Direction.DESCENDING + : Direction.ASCENDING); + structuredQuery.addOrderBy(order.toProto()); + break; + } } + } else if (LimitType.Last.equals(options.getLimitType())) { + throw new IllegalStateException( + "limitToLast() queries require specifying at least one orderBy() clause."); } if (!options.getFieldProjections().isEmpty()) { @@ -1021,11 +1068,39 @@ StructuredQuery.Builder buildQuery() { } if (options.getStartCursor() != null) { - structuredQuery.setStartAt(options.getStartCursor()); + switch (options.getLimitType()) { + case First: + structuredQuery.setStartAt(options.getStartCursor()); + break; + case Last: + // Swap the cursors to match the flipped query ordering. + Cursor cursor = + options + .getStartCursor() + .toBuilder() + .setBefore(!options.getStartCursor().getBefore()) + .build(); + structuredQuery.setEndAt(cursor); + break; + } } if (options.getEndCursor() != null) { - structuredQuery.setEndAt(options.getEndCursor()); + switch (options.getLimitType()) { + case First: + structuredQuery.setEndAt(options.getEndCursor()); + break; + case Last: + // Swap the cursors to match the flipped query ordering. + Cursor cursor = + options + .getEndCursor() + .toBuilder() + .setBefore(!options.getEndCursor().getBefore()) + .build(); + structuredQuery.setStartAt(cursor); + break; + } } return structuredQuery; @@ -1037,7 +1112,12 @@ StructuredQuery.Builder buildQuery() { * @param responseObserver The observer to be notified when results arrive. */ public void stream(@Nonnull final ApiStreamObserver responseObserver) { - stream( + Preconditions.checkState( + !LimitType.Last.equals(Query.this.options.getLimitType()), + "Query results for queries that include limitToLast() constraints cannot be streamed. " + + "Use Query.get() instead."); + + internalStream( new QuerySnapshotObserver() { @Override public void onNext(QueryDocumentSnapshot documentSnapshot) { @@ -1073,7 +1153,7 @@ Timestamp getReadTime() { } } - private void stream( + private void internalStream( final QuerySnapshotObserver documentObserver, @Nullable ByteString transactionId) { RunQueryRequest.Builder request = RunQueryRequest.newBuilder(); request.setStructuredQuery(buildQuery()).setParent(options.getParentPath().toString()); @@ -1178,7 +1258,7 @@ public ListenerRegistration addSnapshotListener( ApiFuture get(@Nullable ByteString transactionId) { final SettableApiFuture result = SettableApiFuture.create(); - stream( + internalStream( new QuerySnapshotObserver() { List documentSnapshots = new ArrayList<>(); @@ -1194,8 +1274,14 @@ public void onError(Throwable throwable) { @Override public void onCompleted() { + // The results for limitToLast queries need to be flipped since we reversed the + // ordering constraints before sending the query to the backend. + List resultView = + LimitType.Last.equals(Query.this.options.getLimitType()) + ? reverse(documentSnapshots) + : documentSnapshots; QuerySnapshot querySnapshot = - QuerySnapshot.withDocuments(Query.this, this.getReadTime(), documentSnapshots); + QuerySnapshot.withDocuments(Query.this, this.getReadTime(), resultView); result.set(querySnapshot); } }, diff --git a/google-cloud-firestore/src/test/java/com/google/cloud/firestore/QueryTest.java b/google-cloud-firestore/src/test/java/com/google/cloud/firestore/QueryTest.java index 4f4f1e689..fa342778d 100644 --- a/google-cloud-firestore/src/test/java/com/google/cloud/firestore/QueryTest.java +++ b/google-cloud-firestore/src/test/java/com/google/cloud/firestore/QueryTest.java @@ -29,6 +29,7 @@ import static com.google.cloud.firestore.LocalFirestoreHelper.reference; import static com.google.cloud.firestore.LocalFirestoreHelper.select; import static com.google.cloud.firestore.LocalFirestoreHelper.startAt; +import static com.google.cloud.firestore.LocalFirestoreHelper.string; import static com.google.cloud.firestore.LocalFirestoreHelper.unaryFilter; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; @@ -92,6 +93,96 @@ public void withLimit() throws Exception { assertEquals(query(limit(42)), runQuery.getValue()); } + @Test + public void limitToLastReversesOrderingConstraints() throws Exception { + doAnswer(queryResponse()) + .when(firestoreMock) + .streamRequest( + runQuery.capture(), + streamObserverCapture.capture(), + Matchers.any()); + + query.orderBy("foo").limitToLast(42).get().get(); + + assertEquals( + query(limit(42), order("foo", StructuredQuery.Direction.DESCENDING)), runQuery.getValue()); + } + + @Test + public void limitToLastReversesCursors() throws Exception { + doAnswer(queryResponse()) + .when(firestoreMock) + .streamRequest( + runQuery.capture(), + streamObserverCapture.capture(), + Matchers.any()); + + query.orderBy("foo").startAt("foo").endAt("bar").limitToLast(42).get().get(); + + assertEquals( + query( + limit(42), + order("foo", StructuredQuery.Direction.DESCENDING), + endAt(string("foo"), false), + startAt(string("bar"), true)), + runQuery.getValue()); + } + + @Test + public void limitToLastReversesResults() throws Exception { + doAnswer(queryResponse(DOCUMENT_NAME + "2", DOCUMENT_NAME + "1")) + .when(firestoreMock) + .streamRequest( + runQuery.capture(), + streamObserverCapture.capture(), + Matchers.any()); + + QuerySnapshot querySnapshot = query.orderBy("foo").limitToLast(2).get().get(); + + Iterator docIterator = querySnapshot.iterator(); + assertEquals("doc1", docIterator.next().getId()); + assertEquals("doc2", docIterator.next().getId()); + } + + @Test + public void limitToLastRequiresAtLeastOneOrderingConstraint() throws Exception { + doAnswer(queryResponse()) + .when(firestoreMock) + .streamRequest( + runQuery.capture(), + streamObserverCapture.capture(), + Matchers.any()); + + try { + query.limitToLast(1).get().get(); + fail("Expected exception"); + } catch (IllegalStateException e) { + assertEquals( + e.getMessage(), + "limitToLast() queries require specifying at least one orderBy() clause."); + } + } + + @Test + public void limitToLastRejectsStream() throws Exception { + doAnswer(queryResponse()) + .when(firestoreMock) + .streamRequest( + runQuery.capture(), + streamObserverCapture.capture(), + Matchers.any()); + + try { + query.orderBy("foo").limitToLast(1).stream(null); + fail("Expected exception"); + } catch (IllegalStateException e) { + assertEquals( + e.getMessage(), + "Query results for queries that include limitToLast() constraints cannot be streamed. " + + "Use Query.get() instead."); + } + } + @Test public void withOffset() throws Exception { doAnswer(queryResponse()) diff --git a/google-cloud-firestore/src/test/java/com/google/cloud/firestore/it/ITQueryWatchTest.java b/google-cloud-firestore/src/test/java/com/google/cloud/firestore/it/ITQueryWatchTest.java index 644488118..69cf5cec8 100644 --- a/google-cloud-firestore/src/test/java/com/google/cloud/firestore/it/ITQueryWatchTest.java +++ b/google-cloud-firestore/src/test/java/com/google/cloud/firestore/it/ITQueryWatchTest.java @@ -17,10 +17,11 @@ package com.google.cloud.firestore.it; import static com.google.cloud.firestore.LocalFirestoreHelper.map; -import static com.google.common.collect.Sets.newHashSet; import static com.google.common.truth.Truth.assertThat; import static com.google.common.truth.Truth.assertWithMessage; -import static java.util.Collections.emptySet; +import static java.util.Arrays.asList; +import static java.util.Collections.emptyList; +import static java.util.Collections.singletonList; import com.google.cloud.firestore.CollectionReference; import com.google.cloud.firestore.DocumentChange; @@ -45,10 +46,8 @@ import java.util.ArrayList; import java.util.Collections; import java.util.EnumMap; -import java.util.HashSet; import java.util.List; import java.util.Map; -import java.util.Set; import java.util.concurrent.CountDownLatch; import java.util.concurrent.ExecutionException; import java.util.concurrent.TimeUnit; @@ -112,9 +111,9 @@ public void emptyResults() throws InterruptedException { ListenerAssertions listenerAssertions = listener.assertions(); listenerAssertions.noError(); listenerAssertions.eventCountIsAnyOf(Range.closed(1, 1)); - listenerAssertions.addedIdsIsAnyOf(emptySet()); - listenerAssertions.modifiedIdsIsAnyOf(emptySet()); - listenerAssertions.removedIdsIsAnyOf(emptySet()); + listenerAssertions.addedIdsIsAnyOf(emptyList()); + listenerAssertions.modifiedIdsIsAnyOf(emptyList()); + listenerAssertions.removedIdsIsAnyOf(emptyList()); } /** @@ -126,9 +125,9 @@ public void emptyResults() throws InterruptedException { * */ @Test - public void nonEmptyResults() throws InterruptedException, TimeoutException, ExecutionException { + public void nonEmptyResults() throws Exception { // create a document in our collection that will match the query - randomColl.document("doc").set(map("foo", "bar")).get(5, TimeUnit.SECONDS); + setDocument("doc", map("foo", "bar")); final Query query = randomColl.whereEqualTo("foo", "bar"); QuerySnapshotEventListener listener = @@ -144,9 +143,9 @@ public void nonEmptyResults() throws InterruptedException, TimeoutException, Exe ListenerAssertions listenerAssertions = listener.assertions(); listenerAssertions.noError(); listenerAssertions.eventCountIsAnyOf(Range.closed(1, 1)); - listenerAssertions.addedIdsIsAnyOf(newHashSet("doc")); - listenerAssertions.modifiedIdsIsAnyOf(emptySet()); - listenerAssertions.removedIdsIsAnyOf(emptySet()); + listenerAssertions.addedIdsIsAnyOf(singletonList("doc")); + listenerAssertions.modifiedIdsIsAnyOf(emptyList()); + listenerAssertions.removedIdsIsAnyOf(emptyList()); } /** @@ -178,9 +177,9 @@ public void emptyResults_newDocument_ADDED() ListenerAssertions listenerAssertions = listener.assertions(); listenerAssertions.noError(); listenerAssertions.eventCountIsAnyOf(Range.closed(2, 2)); - listenerAssertions.addedIdsIsAnyOf(emptySet(), newHashSet("doc")); - listenerAssertions.modifiedIdsIsAnyOf(emptySet()); - listenerAssertions.removedIdsIsAnyOf(emptySet()); + listenerAssertions.addedIdsIsAnyOf(emptyList(), singletonList("doc")); + listenerAssertions.modifiedIdsIsAnyOf(emptyList()); + listenerAssertions.removedIdsIsAnyOf(emptyList()); } /** @@ -193,10 +192,9 @@ public void emptyResults_newDocument_ADDED() * */ @Test - public void emptyResults_modifiedDocument_ADDED() - throws InterruptedException, TimeoutException, ExecutionException { + public void emptyResults_modifiedDocument_ADDED() throws Exception { // create our "existing non-matching document" - randomColl.document("doc").set(map("baz", "baz")).get(5, TimeUnit.SECONDS); + DocumentReference testDoc = setDocument("doc", map("baz", "baz")); final Query query = randomColl.whereEqualTo("foo", "bar"); QuerySnapshotEventListener listener = @@ -206,7 +204,7 @@ public void emptyResults_modifiedDocument_ADDED() try { listener.eventsCountDownLatch.awaitInitialEvents(); - randomColl.document("doc").update("foo", "bar").get(5, TimeUnit.SECONDS); + testDoc.update("foo", "bar").get(5, TimeUnit.SECONDS); listener.eventsCountDownLatch.await(DocumentChange.Type.ADDED); } finally { registration.remove(); @@ -215,9 +213,9 @@ public void emptyResults_modifiedDocument_ADDED() ListenerAssertions listenerAssertions = listener.assertions(); listenerAssertions.noError(); listenerAssertions.eventCountIsAnyOf(Range.closed(2, 2)); - listenerAssertions.addedIdsIsAnyOf(emptySet(), newHashSet("doc")); - listenerAssertions.modifiedIdsIsAnyOf(emptySet()); - listenerAssertions.removedIdsIsAnyOf(emptySet()); + listenerAssertions.addedIdsIsAnyOf(emptyList(), singletonList("doc")); + listenerAssertions.modifiedIdsIsAnyOf(emptyList()); + listenerAssertions.removedIdsIsAnyOf(emptyList()); ListenerEvent event = receivedEvents.get(receivedEvents.size() - 1); //noinspection ConstantConditions guarded by "assertNoError" above @@ -236,11 +234,8 @@ public void emptyResults_modifiedDocument_ADDED() * */ @Test - public void nonEmptyResults_modifiedDocument_MODIFIED() - throws InterruptedException, TimeoutException, ExecutionException { - DocumentReference testDoc = randomColl.document("doc"); - // create our "existing non-matching document" - testDoc.set(map("foo", "bar")).get(5, TimeUnit.SECONDS); + public void nonEmptyResults_modifiedDocument_MODIFIED() throws Exception { + DocumentReference testDoc = setDocument("doc", map("foo", "bar")); final Query query = randomColl.whereEqualTo("foo", "bar"); // register the snapshot listener for the query @@ -263,9 +258,9 @@ public void nonEmptyResults_modifiedDocument_MODIFIED() ListenerAssertions listenerAssertions = listener.assertions(); listenerAssertions.noError(); listenerAssertions.eventCountIsAnyOf(Range.closed(2, 2)); - listenerAssertions.addedIdsIsAnyOf(emptySet(), newHashSet("doc")); - listenerAssertions.modifiedIdsIsAnyOf(emptySet(), newHashSet("doc")); - listenerAssertions.removedIdsIsAnyOf(emptySet()); + listenerAssertions.addedIdsIsAnyOf(emptyList(), singletonList("doc")); + listenerAssertions.modifiedIdsIsAnyOf(emptyList(), singletonList("doc")); + listenerAssertions.removedIdsIsAnyOf(emptyList()); ListenerEvent event = receivedEvents.get(receivedEvents.size() - 1); //noinspection ConstantConditions guarded by "assertNoError" above @@ -284,11 +279,8 @@ public void nonEmptyResults_modifiedDocument_MODIFIED() * */ @Test - public void nonEmptyResults_deletedDocument_REMOVED() - throws InterruptedException, TimeoutException, ExecutionException { - DocumentReference testDoc = randomColl.document("doc"); - // create our "existing non-matching document" - testDoc.set(map("foo", "bar")).get(5, TimeUnit.SECONDS); + public void nonEmptyResults_deletedDocument_REMOVED() throws Exception { + DocumentReference testDoc = setDocument("doc", map("foo", "bar")); final Query query = randomColl.whereEqualTo("foo", "bar"); // register the snapshot listener for the query @@ -311,9 +303,9 @@ public void nonEmptyResults_deletedDocument_REMOVED() ListenerAssertions listenerAssertions = listener.assertions(); listenerAssertions.noError(); listenerAssertions.eventCountIsAnyOf(Range.closed(2, 2)); - listenerAssertions.addedIdsIsAnyOf(emptySet(), newHashSet("doc")); - listenerAssertions.modifiedIdsIsAnyOf(emptySet()); - listenerAssertions.removedIdsIsAnyOf(emptySet(), newHashSet("doc")); + listenerAssertions.addedIdsIsAnyOf(emptyList(), singletonList("doc")); + listenerAssertions.modifiedIdsIsAnyOf(emptyList()); + listenerAssertions.removedIdsIsAnyOf(emptyList(), singletonList("doc")); ListenerEvent event = receivedEvents.get(receivedEvents.size() - 1); //noinspection ConstantConditions guarded by "assertNoError" above @@ -331,11 +323,8 @@ public void nonEmptyResults_deletedDocument_REMOVED() * */ @Test - public void nonEmptyResults_modifiedDocument_REMOVED() - throws InterruptedException, TimeoutException, ExecutionException { - DocumentReference testDoc = randomColl.document("doc"); - // create our "existing non-matching document" - testDoc.set(map("foo", "bar")).get(5, TimeUnit.SECONDS); + public void nonEmptyResults_modifiedDocument_REMOVED() throws Exception { + DocumentReference testDoc = setDocument("doc", map("foo", "bar")); final Query query = randomColl.whereEqualTo("foo", "bar"); // register the snapshot listener for the query @@ -358,9 +347,9 @@ public void nonEmptyResults_modifiedDocument_REMOVED() ListenerAssertions listenerAssertions = listener.assertions(); listenerAssertions.noError(); listenerAssertions.eventCountIsAnyOf(Range.closed(2, 2)); - listenerAssertions.addedIdsIsAnyOf(emptySet(), newHashSet("doc")); - listenerAssertions.modifiedIdsIsAnyOf(emptySet()); - listenerAssertions.removedIdsIsAnyOf(emptySet(), newHashSet("doc")); + listenerAssertions.addedIdsIsAnyOf(emptyList(), singletonList("doc")); + listenerAssertions.modifiedIdsIsAnyOf(emptyList()); + listenerAssertions.removedIdsIsAnyOf(emptyList(), singletonList("doc")); ListenerEvent event = receivedEvents.get(receivedEvents.size() - 1); //noinspection ConstantConditions guarded by "assertNoError" above @@ -368,6 +357,29 @@ public void nonEmptyResults_modifiedDocument_REMOVED() assertThat(doc.get("foo")).isEqualTo("bar"); } + /** Verifies that QuerySnapshot for limitToLast() queries are ordered correctly. */ + @Test + public void limitToLast() throws Exception { + setDocument("doc1", Collections.singletonMap("counter", 1)); + setDocument("doc2", Collections.singletonMap("counter", 2)); + setDocument("doc3", Collections.singletonMap("counter", 3)); + + final Query query = randomColl.orderBy("counter").limitToLast(2); + QuerySnapshotEventListener listener = + QuerySnapshotEventListener.builder().setInitialEventCount(1).build(); + ListenerRegistration registration = query.addSnapshotListener(listener); + + try { + listener.eventsCountDownLatch.awaitInitialEvents(); + } finally { + registration.remove(); + } + + ListenerAssertions listenerAssertions = listener.assertions(); + listenerAssertions.noError(); + listenerAssertions.addedIdsIsAnyOf(emptyList(), asList("doc2", "doc3")); + } + /** * A tuple class used by {@code #queryWatch}. This class represents an event delivered to the * registered query listener. @@ -496,9 +508,9 @@ public QuerySnapshotEventListener build() { static final class ListenerAssertions { private static final MapJoiner MAP_JOINER = Joiner.on(",").withKeyValueSeparator("="); private final FluentIterable events; - private final Set addedIds; - private final Set modifiedIds; - private final Set removedIds; + private final List addedIds; + private final List modifiedIds; + private final List removedIds; ListenerAssertions(List receivedEvents) { events = FluentIterable.from(receivedEvents); @@ -539,9 +551,9 @@ public QuerySnapshot apply(ListenerEvent input) { .toList(); } - private static Set getIds( + private static List getIds( List querySnapshots, DocumentChange.Type type) { - final Set documentIds = new HashSet<>(); + final List documentIds = new ArrayList<>(); for (QuerySnapshot querySnapshot : querySnapshots) { final List changes = querySnapshot.getDocumentChanges(); for (DocumentChange change : changes) { @@ -553,27 +565,27 @@ private static Set getIds( return documentIds; } - void addedIdsIsAnyOf(Set s) { + void addedIdsIsAnyOf(List s) { Truth.assertWithMessage(debugMessage()).that(addedIds).isEqualTo(s); } - void addedIdsIsAnyOf(Set s1, Set s2) { + void addedIdsIsAnyOf(List s1, List s2) { Truth.assertWithMessage(debugMessage()).that(addedIds).isAnyOf(s1, s2); } - void modifiedIdsIsAnyOf(Set s) { + void modifiedIdsIsAnyOf(List s) { Truth.assertWithMessage(debugMessage()).that(modifiedIds).isEqualTo(s); } - void modifiedIdsIsAnyOf(Set s1, Set s2) { + void modifiedIdsIsAnyOf(List s1, List s2) { Truth.assertWithMessage(debugMessage()).that(modifiedIds).isAnyOf(s1, s2); } - void removedIdsIsAnyOf(Set s) { + void removedIdsIsAnyOf(List s) { Truth.assertWithMessage(debugMessage()).that(removedIds).isEqualTo(s); } - void removedIdsIsAnyOf(Set s1, Set s2) { + void removedIdsIsAnyOf(List s1, List s2) { Truth.assertWithMessage(debugMessage()).that(removedIds).isAnyOf(s1, s2); } @@ -638,4 +650,10 @@ private static void debugMessage(StringBuilder builder, Map data } } } + + private DocumentReference setDocument(String documentId, Map fields) throws Exception { + DocumentReference documentReference = randomColl.document(documentId); + documentReference.set(fields).get(); + return documentReference; + } } diff --git a/google-cloud-firestore/src/test/java/com/google/cloud/firestore/it/ITSystemTest.java b/google-cloud-firestore/src/test/java/com/google/cloud/firestore/it/ITSystemTest.java index f5f9c1e24..a0fd2bef9 100644 --- a/google-cloud-firestore/src/test/java/com/google/cloud/firestore/it/ITSystemTest.java +++ b/google-cloud-firestore/src/test/java/com/google/cloud/firestore/it/ITSystemTest.java @@ -105,8 +105,7 @@ public void after() throws Exception { firestore.close(); } - private DocumentReference setDocument(String documentId, Map fields) - throws Exception { + private DocumentReference setDocument(String documentId, Map fields) throws Exception { DocumentReference documentReference = randomColl.document(documentId); documentReference.set(fields).get(); return documentReference; @@ -229,7 +228,7 @@ public void setDocumentWithMerge() throws Exception { @Test public void mergeDocumentWithServerTimestamp() throws Exception { Map originalMap = LocalFirestoreHelper.map("a", "b"); - Map updateMap = map("c", (Object) FieldValue.serverTimestamp()); + Map updateMap = map("c", FieldValue.serverTimestamp()); randomDoc.set(originalMap).get(); randomDoc.set(updateMap, SetOptions.merge()).get(); DocumentSnapshot documentSnapshot = randomDoc.get().get(); @@ -356,12 +355,22 @@ public void nestedQuery() throws Exception { @Test public void limitQuery() throws Exception { - addDocument("foo", "bar"); - addDocument("foo", "bar"); + setDocument("doc1", Collections.singletonMap("counter", 1)); + setDocument("doc2", Collections.singletonMap("counter", 2)); + setDocument("doc3", Collections.singletonMap("counter", 3)); - QuerySnapshot querySnapshot = randomColl.limit(1).get().get(); - assertEquals(1, querySnapshot.size()); - assertEquals("bar", querySnapshot.getDocuments().get(0).get("foo")); + QuerySnapshot querySnapshot = randomColl.orderBy("counter").limit(2).get().get(); + assertEquals(asList("doc1", "doc2"), querySnapshotToIds(querySnapshot)); + } + + @Test + public void limitToLastQuery() throws Exception { + setDocument("doc1", Collections.singletonMap("counter", 1)); + setDocument("doc2", Collections.singletonMap("counter", 2)); + setDocument("doc3", Collections.singletonMap("counter", 3)); + + QuerySnapshot querySnapshot = randomColl.orderBy("counter").limitToLast(2).get().get(); + assertEquals(asList("doc2", "doc3"), querySnapshotToIds(querySnapshot)); } @Test @@ -950,13 +959,13 @@ public void arrayOperators() throws ExecutionException, InterruptedException { DocumentReference doc1 = randomColl.document(); DocumentReference doc2 = randomColl.document(); - doc1.set(Collections.singletonMap("foo", (Object) FieldValue.arrayUnion("bar"))).get(); - doc2.set(Collections.singletonMap("foo", (Object) FieldValue.arrayUnion("baz"))).get(); + doc1.set(Collections.singletonMap("foo", FieldValue.arrayUnion("bar"))).get(); + doc2.set(Collections.singletonMap("foo", FieldValue.arrayUnion("baz"))).get(); assertEquals(1, containsQuery.get().get().size()); - doc1.set(Collections.singletonMap("foo", (Object) FieldValue.arrayRemove("bar"))).get(); - doc2.set(Collections.singletonMap("foo", (Object) FieldValue.arrayRemove("baz"))).get(); + doc1.set(Collections.singletonMap("foo", FieldValue.arrayRemove("bar"))).get(); + doc2.set(Collections.singletonMap("foo", FieldValue.arrayRemove("baz"))).get(); assertTrue(containsQuery.get().get().isEmpty()); } @@ -1085,12 +1094,12 @@ public void collectionGroupQueriesWithWhereFiltersOnArbitraryDocumentIds() @Test public void inQueries() throws Exception { - setDocument("a", map("zip", (Object) 98101)); - setDocument("b", map("zip", (Object) 91102)); - setDocument("c", map("zip", (Object) 98103)); - setDocument("d", map("zip", (Object) asList(98101))); - setDocument("e", map("zip", (Object) asList("98101", map("zip", 98101)))); - setDocument("f", map("zip", (Object) map("code", 500))); + setDocument("a", map("zip", 98101)); + setDocument("b", map("zip", 91102)); + setDocument("c", map("zip", 98103)); + setDocument("d", map("zip", asList(98101))); + setDocument("e", map("zip", asList("98101", map("zip", 98101)))); + setDocument("f", map("zip", map("code", 500))); QuerySnapshot querySnapshot = randomColl.whereIn("zip", Arrays.asList(98101, 98103)).get().get(); @@ -1100,13 +1109,13 @@ public void inQueries() throws Exception { @Test public void arrayContainsAnyQueries() throws Exception { - setDocument("a", map("array", (Object) asList(42))); - setDocument("b", map("array", (Object) asList("a", 42, "c"))); - setDocument("c", map("array", (Object) asList(41.999, "42", map("a", 42)))); - setDocument("d", map("array", (Object) asList(42), "array2", "sigh")); - setDocument("e", map("array", (Object) asList(43))); - setDocument("f", map("array", (Object) asList(map("a", 42)))); - setDocument("g", map("array", (Object) 42)); + setDocument("a", map("array", asList(42))); + setDocument("b", map("array", asList("a", 42, "c"))); + setDocument("c", map("array", asList(41.999, "42", map("a", 42)))); + setDocument("d", map("array", asList(42), "array2", "sigh")); + setDocument("e", map("array", asList(43))); + setDocument("f", map("array", asList(map("a", 42)))); + setDocument("g", map("array", 42)); QuerySnapshot querySnapshot = randomColl.whereArrayContainsAny("array", Arrays.asList(42, 43)).get().get(); @@ -1117,7 +1126,7 @@ public void arrayContainsAnyQueries() throws Exception { @Test public void integerIncrement() throws ExecutionException, InterruptedException { DocumentReference docRef = randomColl.document(); - docRef.set(Collections.singletonMap("sum", (Object) 1L)).get(); + docRef.set(Collections.singletonMap("sum", 1L)).get(); docRef.update("sum", FieldValue.increment(2)).get(); DocumentSnapshot docSnap = docRef.get().get(); assertEquals(3L, docSnap.get("sum")); @@ -1126,7 +1135,7 @@ public void integerIncrement() throws ExecutionException, InterruptedException { @Test public void floatIncrement() throws ExecutionException, InterruptedException { DocumentReference docRef = randomColl.document(); - docRef.set(Collections.singletonMap("sum", (Object) 1.1)).get(); + docRef.set(Collections.singletonMap("sum", 1.1)).get(); docRef.update("sum", FieldValue.increment(2.2)).get(); DocumentSnapshot docSnap = docRef.get().get(); assertEquals(3.3, (Double) docSnap.get("sum"), DOUBLE_EPSILON);