Skip to content

Commit c38e3ad

Browse files
authored
Fix: sort document reference by long type id (#1945)
1 parent 126e745 commit c38e3ad

File tree

3 files changed

+147
-3
lines changed

3 files changed

+147
-3
lines changed

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ If you are using Maven without the BOM, add this to your dependencies:
4141
<dependency>
4242
<groupId>com.google.cloud</groupId>
4343
<artifactId>google-cloud-firestore</artifactId>
44-
<version>3.30.1</version>
44+
<version>3.30.2</version>
4545
</dependency>
4646

4747
```

google-cloud-firestore/src/main/java/com/google/cloud/firestore/BasePath.java

Lines changed: 31 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -114,7 +114,11 @@ boolean isPrefixOf(BasePath<B> path) {
114114
}
115115

116116
/**
117-
* Compare the current path lexicographically against another Path object.
117+
* Compare the current path against another Path object.
118+
*
119+
* <p>Compare the current path against another Path object. Paths are compared segment by segment,
120+
* prioritizing numeric IDs (e.g., "__id123__") in numeric ascending order, followed by string
121+
* segments in lexicographical order.
118122
*
119123
* @param other The path to compare to.
120124
* @return -1 if current is less than other, 1 if current greater than other, 0 if equal
@@ -123,16 +127,41 @@ boolean isPrefixOf(BasePath<B> path) {
123127
public int compareTo(@Nonnull B other) {
124128
List<String> thisSegments = this.getSegments();
125129
List<String> otherSegments = other.getSegments();
130+
126131
int length = Math.min(thisSegments.size(), otherSegments.size());
127132
for (int i = 0; i < length; i++) {
128-
int cmp = thisSegments.get(i).compareTo(otherSegments.get(i));
133+
int cmp = compareSegments(thisSegments.get(i), otherSegments.get(i));
129134
if (cmp != 0) {
130135
return cmp;
131136
}
132137
}
133138
return Integer.compare(thisSegments.size(), otherSegments.size());
134139
}
135140

141+
private int compareSegments(String lhs, String rhs) {
142+
boolean isLhsNumeric = isNumericId(lhs);
143+
boolean isRhsNumeric = isNumericId(rhs);
144+
145+
if (isLhsNumeric && !isRhsNumeric) { // Only lhs is numeric
146+
return -1;
147+
} else if (!isLhsNumeric && isRhsNumeric) { // Only rhs is numeric
148+
return 1;
149+
} else if (isLhsNumeric && isRhsNumeric) { // both numeric
150+
return Long.compare(extractNumericId(lhs), extractNumericId(rhs));
151+
} else { // both string
152+
return lhs.compareTo(rhs);
153+
}
154+
}
155+
156+
/** Checks if a segment is a numeric ID (starts with "__id" and ends with "__"). */
157+
private boolean isNumericId(String segment) {
158+
return segment.startsWith("__id") && segment.endsWith("__");
159+
}
160+
161+
private long extractNumericId(String segment) {
162+
return Long.parseLong(segment.substring(4, segment.length() - 2));
163+
}
164+
136165
abstract String[] splitChildPath(String path);
137166

138167
abstract B createPathWithSegments(ImmutableList<String> segments);

google-cloud-firestore/src/test/java/com/google/cloud/firestore/it/ITQueryWatchTest.java

Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,17 +24,20 @@
2424
import static java.util.Collections.emptyList;
2525
import static java.util.Collections.singletonList;
2626
import static org.junit.Assert.assertArrayEquals;
27+
import static org.junit.Assert.assertEquals;
2728

2829
import com.google.cloud.firestore.CollectionReference;
2930
import com.google.cloud.firestore.DocumentChange;
3031
import com.google.cloud.firestore.DocumentReference;
3132
import com.google.cloud.firestore.DocumentSnapshot;
3233
import com.google.cloud.firestore.EventListener;
34+
import com.google.cloud.firestore.FieldPath;
3335
import com.google.cloud.firestore.FieldValue;
3436
import com.google.cloud.firestore.FirestoreException;
3537
import com.google.cloud.firestore.ListenerRegistration;
3638
import com.google.cloud.firestore.LocalFirestoreHelper;
3739
import com.google.cloud.firestore.Query;
40+
import com.google.cloud.firestore.Query.Direction;
3841
import com.google.cloud.firestore.QueryDocumentSnapshot;
3942
import com.google.cloud.firestore.QuerySnapshot;
4043
import com.google.cloud.firestore.it.ITQueryWatchTest.QuerySnapshotEventListener.ListenerAssertions;
@@ -644,6 +647,118 @@ public void shutdownNowPreventsAddingNewListener() throws Exception {
644647
listenerAssertions.hasError();
645648
}
646649

650+
@Test
651+
public void snapshotListenerSortsQueryByDocumentIdInTheSameOrderAsServer() throws Exception {
652+
CollectionReference col = randomColl;
653+
654+
firestore
655+
.batch()
656+
.set(col.document("A"), Collections.singletonMap("a", 1))
657+
.set(col.document("a"), Collections.singletonMap("a", 1))
658+
.set(col.document("Aa"), Collections.singletonMap("a", 1))
659+
.set(col.document("7"), Collections.singletonMap("a", 1))
660+
.set(col.document("12"), Collections.singletonMap("a", 1))
661+
.set(col.document("__id7__"), Collections.singletonMap("a", 1))
662+
.set(col.document("__id12__"), Collections.singletonMap("a", 1))
663+
.set(col.document("__id-2__"), Collections.singletonMap("a", 1))
664+
.set(col.document("_id1__"), Collections.singletonMap("a", 1))
665+
.set(col.document("__id1_"), Collections.singletonMap("a", 1))
666+
.set(col.document("__id"), Collections.singletonMap("a", 1))
667+
.commit()
668+
.get();
669+
670+
Query query = col.orderBy("__name__", Direction.ASCENDING);
671+
List<String> expectedOrder =
672+
Arrays.asList(
673+
"__id-2__",
674+
"__id7__",
675+
"__id12__",
676+
"12",
677+
"7",
678+
"A",
679+
"Aa",
680+
"__id",
681+
"__id1_",
682+
"_id1__",
683+
"a");
684+
685+
QuerySnapshot snapshot = query.get().get();
686+
List<String> queryOrder =
687+
snapshot.getDocuments().stream().map(doc -> doc.getId()).collect(Collectors.toList());
688+
assertEquals(expectedOrder, queryOrder); // Assert order from backend
689+
690+
CountDownLatch latch = new CountDownLatch(1);
691+
List<String> listenerOrder = new ArrayList<>();
692+
693+
ListenerRegistration registration =
694+
query.addSnapshotListener(
695+
(value, error) -> {
696+
listenerOrder.addAll(
697+
value.getDocuments().stream()
698+
.map(doc -> doc.getId())
699+
.collect(Collectors.toList()));
700+
701+
latch.countDown();
702+
});
703+
704+
latch.await();
705+
registration.remove();
706+
707+
assertEquals(expectedOrder, listenerOrder); // Assert order in the SDK
708+
}
709+
710+
@Test
711+
public void snapshotListenerSortsFilteredQueryByDocumentIdInTheSameOrderAsServer()
712+
throws Exception {
713+
CollectionReference col = randomColl;
714+
715+
firestore
716+
.batch()
717+
.set(col.document("A"), Collections.singletonMap("a", 1))
718+
.set(col.document("a"), Collections.singletonMap("a", 1))
719+
.set(col.document("Aa"), Collections.singletonMap("a", 1))
720+
.set(col.document("7"), Collections.singletonMap("a", 1))
721+
.set(col.document("12"), Collections.singletonMap("a", 1))
722+
.set(col.document("__id7__"), Collections.singletonMap("a", 1))
723+
.set(col.document("__id12__"), Collections.singletonMap("a", 1))
724+
.set(col.document("__id-2__"), Collections.singletonMap("a", 1))
725+
.set(col.document("_id1__"), Collections.singletonMap("a", 1))
726+
.set(col.document("__id1_"), Collections.singletonMap("a", 1))
727+
.set(col.document("__id"), Collections.singletonMap("a", 1))
728+
.commit()
729+
.get();
730+
731+
Query query =
732+
col.whereGreaterThan(FieldPath.documentId(), "__id7__")
733+
.whereLessThanOrEqualTo(FieldPath.documentId(), "A")
734+
.orderBy("__name__", Direction.ASCENDING);
735+
List<String> expectedOrder = Arrays.asList("__id12__", "12", "7", "A");
736+
737+
QuerySnapshot snapshot = query.get().get();
738+
List<String> queryOrder =
739+
snapshot.getDocuments().stream().map(doc -> doc.getId()).collect(Collectors.toList());
740+
assertEquals(expectedOrder, queryOrder); // Assert order from backend
741+
742+
CountDownLatch latch = new CountDownLatch(1);
743+
List<String> listenerOrder = new ArrayList<>();
744+
745+
ListenerRegistration registration =
746+
query.addSnapshotListener(
747+
(value, error) -> {
748+
listenerOrder.addAll(
749+
value.getDocuments().stream()
750+
.map(doc -> doc.getId())
751+
.collect(Collectors.toList()));
752+
753+
latch.countDown();
754+
});
755+
756+
latch.await();
757+
registration.remove();
758+
759+
assertEquals(expectedOrder, listenerOrder); // Assert order in the SDK
760+
}
761+
647762
/**
648763
* A tuple class used by {@code #queryWatch}. This class represents an event delivered to the
649764
* registered query listener.

0 commit comments

Comments
 (0)