Skip to content
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ If you are using Maven without the BOM, add this to your dependencies:
<dependency>
<groupId>com.google.cloud</groupId>
<artifactId>google-cloud-firestore</artifactId>
<version>3.29.1</version>
<version>3.30.0</version>
</dependency>

```
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -114,7 +114,11 @@ boolean isPrefixOf(BasePath<B> path) {
}

/**
* Compare the current path lexicographically against another Path object.
* Compare the current path against another Path object.
*
* <p>Compare the current path against another Path object. Paths are compared segment by segment,
* prioritizing numeric IDs (e.g., "__id123__") in ascending order, followed by string segments in
* lexicographical order.
*
* @param other The path to compare to.
* @return -1 if current is less than other, 1 if current greater than other, 0 if equal
Expand All @@ -123,16 +127,44 @@ boolean isPrefixOf(BasePath<B> path) {
public int compareTo(@Nonnull B other) {
List<String> thisSegments = this.getSegments();
List<String> otherSegments = other.getSegments();

int length = Math.min(thisSegments.size(), otherSegments.size());
for (int i = 0; i < length; i++) {
int cmp = thisSegments.get(i).compareTo(otherSegments.get(i));
int cmp = compareSegments(thisSegments.get(i), otherSegments.get(i));
if (cmp != 0) {
return cmp;
}
}
return Integer.compare(thisSegments.size(), otherSegments.size());
}

private int compareSegments(String segment1, String segment2) {
// 1. Check if one segment is numeric and the other is not
if (isNumericId(segment1) && !isNumericId(segment2)) {
return -1;
} else if (!isNumericId(segment1) && isNumericId(segment2)) {
return 1;
}

// 2. If both are numeric, compare numerically
if (isNumericId(segment1) && isNumericId(segment2)) {
return Long.compare(extractNumericId(segment1), extractNumericId(segment2));
}

// 3. If both are strings, compare lexicographically
return segment1.compareTo(segment2);
}

// Checks if a segment is a numeric ID (starts with "__id" and ends with "__").
private boolean isNumericId(String segment) {
return segment.startsWith("__id") && segment.endsWith("__");
}

// Extracts the numeric value from a numeric ID segment.
private long extractNumericId(String segment) {
return Long.parseLong(segment.substring(4, segment.length() - 2));
}

abstract String[] splitChildPath(String path);

abstract B createPathWithSegments(ImmutableList<String> segments);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,17 +24,20 @@
import static java.util.Collections.emptyList;
import static java.util.Collections.singletonList;
import static org.junit.Assert.assertArrayEquals;
import static org.junit.Assert.assertEquals;

import com.google.cloud.firestore.CollectionReference;
import com.google.cloud.firestore.DocumentChange;
import com.google.cloud.firestore.DocumentReference;
import com.google.cloud.firestore.DocumentSnapshot;
import com.google.cloud.firestore.EventListener;
import com.google.cloud.firestore.FieldPath;
import com.google.cloud.firestore.FieldValue;
import com.google.cloud.firestore.FirestoreException;
import com.google.cloud.firestore.ListenerRegistration;
import com.google.cloud.firestore.LocalFirestoreHelper;
import com.google.cloud.firestore.Query;
import com.google.cloud.firestore.Query.Direction;
import com.google.cloud.firestore.QueryDocumentSnapshot;
import com.google.cloud.firestore.QuerySnapshot;
import com.google.cloud.firestore.it.ITQueryWatchTest.QuerySnapshotEventListener.ListenerAssertions;
Expand Down Expand Up @@ -644,6 +647,98 @@ public void shutdownNowPreventsAddingNewListener() throws Exception {
listenerAssertions.hasError();
}

@Test
public void snapshotListenerSortsQueryByDocumentIdInTheSameOrderAsServer() throws Exception {
CollectionReference col = randomColl;

firestore
.batch()
.set(col.document("A"), Collections.singletonMap("a", 1))
.set(col.document("a"), Collections.singletonMap("a", 1))
.set(col.document("Aa"), Collections.singletonMap("a", 1))
.set(col.document("7"), Collections.singletonMap("a", 1))
.set(col.document("12"), Collections.singletonMap("a", 1))
.set(col.document("__id7__"), Collections.singletonMap("a", 1))
.set(col.document("__id12__"), Collections.singletonMap("a", 1))
.commit()
.get();

Query query = col.orderBy("__name__", Direction.ASCENDING);
List<String> expectedOrder = Arrays.asList("__id7__", "__id12__", "12", "7", "A", "Aa", "a");

QuerySnapshot snapshot = query.get().get();
List<String> queryOrder =
snapshot.getDocuments().stream().map(doc -> doc.getId()).collect(Collectors.toList());
assertEquals(expectedOrder, queryOrder); // Assert order from backend

CountDownLatch latch = new CountDownLatch(1);
List<String> listenerOrder = new ArrayList<>();

ListenerRegistration registration =
query.addSnapshotListener(
(value, error) -> {
listenerOrder.addAll(
value.getDocuments().stream()
.map(doc -> doc.getId())
.collect(Collectors.toList()));

latch.countDown();
});

latch.await();
registration.remove();

assertEquals(expectedOrder, listenerOrder); // Assert order in the SDK
}

@Test
public void snapshotListenerSortsFilteredQueryByDocumentIdInTheSameOrderAsServer()
throws Exception {
CollectionReference col = randomColl;

firestore
.batch()
.set(col.document("A"), Collections.singletonMap("a", 1))
.set(col.document("a"), Collections.singletonMap("a", 1))
.set(col.document("Aa"), Collections.singletonMap("a", 1))
.set(col.document("7"), Collections.singletonMap("a", 1))
.set(col.document("12"), Collections.singletonMap("a", 1))
.set(col.document("__id7__"), Collections.singletonMap("a", 1))
.set(col.document("__id12__"), Collections.singletonMap("a", 1))
.commit()
.get();

Query query =
col.whereGreaterThan(FieldPath.documentId(), "__id7__")
.whereLessThanOrEqualTo(FieldPath.documentId(), "A")
.orderBy("__name__", Direction.ASCENDING);
List<String> expectedOrder = Arrays.asList("__id12__", "12", "7", "A");

QuerySnapshot snapshot = query.get().get();
List<String> queryOrder =
snapshot.getDocuments().stream().map(doc -> doc.getId()).collect(Collectors.toList());
assertEquals(expectedOrder, queryOrder); // Assert order from backend

CountDownLatch latch = new CountDownLatch(1);
List<String> listenerOrder = new ArrayList<>();

ListenerRegistration registration =
query.addSnapshotListener(
(value, error) -> {
listenerOrder.addAll(
value.getDocuments().stream()
.map(doc -> doc.getId())
.collect(Collectors.toList()));

latch.countDown();
});

latch.await();
registration.remove();

assertEquals(expectedOrder, listenerOrder); // Assert order in the SDK
}

/**
* A tuple class used by {@code #queryWatch}. This class represents an event delivered to the
* registered query listener.
Expand Down
Loading