Skip to content

Commit 3dd8ddf

Browse files
authored
Fix: sort document reference by long type id (#6594)
1 parent 50eacb7 commit 3dd8ddf

File tree

2 files changed

+190
-1
lines changed

2 files changed

+190
-1
lines changed

firebase-firestore/src/androidTest/java/com/google/firebase/firestore/FirestoreTest.java

Lines changed: 160 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
package com.google.firebase.firestore;
1616

1717
import static com.google.firebase.firestore.AccessHelper.getAsyncQueue;
18+
import static com.google.firebase.firestore.testutil.IntegrationTestUtil.checkOnlineAndOfflineResultsMatch;
1819
import static com.google.firebase.firestore.testutil.IntegrationTestUtil.isRunningAgainstEmulator;
1920
import static com.google.firebase.firestore.testutil.IntegrationTestUtil.newTestSettings;
2021
import static com.google.firebase.firestore.testutil.IntegrationTestUtil.provider;
@@ -63,6 +64,7 @@
6364
import java.util.Map;
6465
import java.util.concurrent.CountDownLatch;
6566
import java.util.concurrent.Semaphore;
67+
import java.util.stream.Collectors;
6668
import org.junit.After;
6769
import org.junit.Test;
6870
import org.junit.runner.RunWith;
@@ -1493,4 +1495,162 @@ public void testCanGetSameOrDifferentPersistentCacheIndexManager() {
14931495
assertNotSame(indexManager3, indexManager6);
14941496
assertNotSame(indexManager5, indexManager6);
14951497
}
1498+
1499+
@Test
1500+
public void snapshotListenerSortsQueryByDocumentIdsSameAsGetQuery() {
1501+
Map<String, Map<String, Object>> testDocs =
1502+
map(
1503+
"A", map("a", 1),
1504+
"a", map("a", 1),
1505+
"Aa", map("a", 1),
1506+
"7", map("a", 1),
1507+
"12", map("a", 1),
1508+
"__id7__", map("a", 1),
1509+
"__id12__", map("a", 1),
1510+
"__id-2__", map("a", 1),
1511+
"__id1_", map("a", 1),
1512+
"_id1__", map("a", 1),
1513+
"__id", map("a", 1),
1514+
"__id9223372036854775807__", map("a", 1),
1515+
"__id-9223372036854775808__", map("a", 1));
1516+
1517+
CollectionReference colRef = testCollectionWithDocs(testDocs);
1518+
1519+
// Run get query
1520+
Query orderedQuery = colRef.orderBy(FieldPath.documentId());
1521+
List<String> expectedDocIds =
1522+
Arrays.asList(
1523+
"__id-9223372036854775808__",
1524+
"__id-2__",
1525+
"__id7__",
1526+
"__id12__",
1527+
"__id9223372036854775807__",
1528+
"12",
1529+
"7",
1530+
"A",
1531+
"Aa",
1532+
"__id",
1533+
"__id1_",
1534+
"_id1__",
1535+
"a");
1536+
1537+
QuerySnapshot getSnapshot = waitFor(orderedQuery.get());
1538+
List<String> getSnapshotDocIds =
1539+
getSnapshot.getDocuments().stream().map(ds -> ds.getId()).collect(Collectors.toList());
1540+
1541+
// Run query with snapshot listener
1542+
EventAccumulator<QuerySnapshot> eventAccumulator = new EventAccumulator<QuerySnapshot>();
1543+
ListenerRegistration registration =
1544+
orderedQuery.addSnapshotListener(eventAccumulator.listener());
1545+
1546+
List<String> watchSnapshotDocIds = new ArrayList<>();
1547+
try {
1548+
QuerySnapshot watchSnapshot = eventAccumulator.await();
1549+
watchSnapshotDocIds =
1550+
watchSnapshot.getDocuments().stream()
1551+
.map(documentSnapshot -> documentSnapshot.getId())
1552+
.collect(Collectors.toList());
1553+
} finally {
1554+
registration.remove();
1555+
}
1556+
1557+
// Assert that get and snapshot listener requests sort docs in the same, expected order
1558+
assertTrue(getSnapshotDocIds.equals(expectedDocIds));
1559+
assertTrue(watchSnapshotDocIds.equals(expectedDocIds));
1560+
}
1561+
1562+
@Test
1563+
public void snapshotListenerSortsFilteredQueryByDocumentIdsSameAsGetQuery() {
1564+
Map<String, Map<String, Object>> testDocs =
1565+
map(
1566+
"A", map("a", 1),
1567+
"a", map("a", 1),
1568+
"Aa", map("a", 1),
1569+
"7", map("a", 1),
1570+
"12", map("a", 1),
1571+
"__id7__", map("a", 1),
1572+
"__id12__", map("a", 1),
1573+
"__id-2__", map("a", 1),
1574+
"__id1_", map("a", 1),
1575+
"_id1__", map("a", 1),
1576+
"__id", map("a", 1),
1577+
"__id9223372036854775807__", map("a", 1),
1578+
"__id-9223372036854775808__", map("a", 1));
1579+
1580+
CollectionReference colRef = testCollectionWithDocs(testDocs);
1581+
1582+
// Run get query
1583+
Query filteredQuery =
1584+
colRef
1585+
.whereGreaterThan(FieldPath.documentId(), "__id7__")
1586+
.whereLessThanOrEqualTo(FieldPath.documentId(), "A")
1587+
.orderBy(FieldPath.documentId());
1588+
List<String> expectedDocIds =
1589+
Arrays.asList("__id12__", "__id9223372036854775807__", "12", "7", "A");
1590+
1591+
QuerySnapshot getSnapshot = waitFor(filteredQuery.get());
1592+
List<String> getSnapshotDocIds =
1593+
getSnapshot.getDocuments().stream().map(ds -> ds.getId()).collect(Collectors.toList());
1594+
1595+
// Run query with snapshot listener
1596+
EventAccumulator<QuerySnapshot> eventAccumulator = new EventAccumulator<QuerySnapshot>();
1597+
ListenerRegistration registration =
1598+
filteredQuery.addSnapshotListener(eventAccumulator.listener());
1599+
1600+
List<String> watchSnapshotDocIds = new ArrayList<>();
1601+
try {
1602+
QuerySnapshot watchSnapshot = eventAccumulator.await();
1603+
watchSnapshotDocIds =
1604+
watchSnapshot.getDocuments().stream()
1605+
.map(documentSnapshot -> documentSnapshot.getId())
1606+
.collect(Collectors.toList());
1607+
} finally {
1608+
registration.remove();
1609+
}
1610+
1611+
// Assert that get and snapshot listener requests sort docs in the same, expected order
1612+
assertTrue(getSnapshotDocIds.equals(expectedDocIds));
1613+
assertTrue(watchSnapshotDocIds.equals(expectedDocIds));
1614+
}
1615+
1616+
@Test
1617+
public void sdkOrdersQueryByDocumentIdTheSameWayOnlineAndOffline() {
1618+
Map<String, Map<String, Object>> testDocs =
1619+
map(
1620+
"A", map("a", 1),
1621+
"a", map("a", 1),
1622+
"Aa", map("a", 1),
1623+
"7", map("a", 1),
1624+
"12", map("a", 1),
1625+
"__id7__", map("a", 1),
1626+
"__id12__", map("a", 1),
1627+
"__id-2__", map("a", 1),
1628+
"__id1_", map("a", 1),
1629+
"_id1__", map("a", 1),
1630+
"__id", map("a", 1),
1631+
"__id9223372036854775807__", map("a", 1),
1632+
"__id-9223372036854775808__", map("a", 1));
1633+
1634+
CollectionReference colRef = testCollectionWithDocs(testDocs);
1635+
// Test query
1636+
Query orderedQuery = colRef.orderBy(FieldPath.documentId());
1637+
List<String> expectedDocIds =
1638+
Arrays.asList(
1639+
"__id-9223372036854775808__",
1640+
"__id-2__",
1641+
"__id7__",
1642+
"__id12__",
1643+
"__id9223372036854775807__",
1644+
"12",
1645+
"7",
1646+
"A",
1647+
"Aa",
1648+
"__id",
1649+
"__id1_",
1650+
"_id1__",
1651+
"a");
1652+
1653+
// Run query with snapshot listener
1654+
checkOnlineAndOfflineResultsMatch(orderedQuery, expectedDocIds.toArray(new String[0]));
1655+
}
14961656
}

firebase-firestore/src/main/java/com/google/firebase/firestore/model/BasePath.java

Lines changed: 30 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -83,13 +83,18 @@ public B keepFirst(int count) {
8383
return createPathWithSegments(segments.subList(0, count));
8484
}
8585

86+
/**
87+
* Compare the current path against another Path object. Paths are compared segment by segment,
88+
* prioritizing numeric IDs (e.g., "__id123__") in numeric ascending order, followed by string
89+
* segments in lexicographical order.
90+
*/
8691
@Override
8792
public int compareTo(@NonNull B o) {
8893
int i = 0;
8994
int myLength = length();
9095
int theirLength = o.length();
9196
while (i < myLength && i < theirLength) {
92-
int localCompare = getSegment(i).compareTo(o.getSegment(i));
97+
int localCompare = compareSegments(getSegment(i), o.getSegment(i));
9398
if (localCompare != 0) {
9499
return localCompare;
95100
}
@@ -98,6 +103,30 @@ public int compareTo(@NonNull B o) {
98103
return Util.compareIntegers(myLength, theirLength);
99104
}
100105

106+
private static int compareSegments(String lhs, String rhs) {
107+
boolean isLhsNumeric = isNumericId(lhs);
108+
boolean isRhsNumeric = isNumericId(rhs);
109+
110+
if (isLhsNumeric && !isRhsNumeric) { // Only lhs is numeric
111+
return -1;
112+
} else if (!isLhsNumeric && isRhsNumeric) { // Only rhs is numeric
113+
return 1;
114+
} else if (isLhsNumeric && isRhsNumeric) { // both numeric
115+
return Long.compare(extractNumericId(lhs), extractNumericId(rhs));
116+
} else { // both string
117+
return lhs.compareTo(rhs);
118+
}
119+
}
120+
121+
/** Checks if a segment is a numeric ID (starts with "__id" and ends with "__"). */
122+
private static boolean isNumericId(String segment) {
123+
return segment.startsWith("__id") && segment.endsWith("__");
124+
}
125+
126+
private static long extractNumericId(String segment) {
127+
return Long.parseLong(segment.substring(4, segment.length() - 2));
128+
}
129+
101130
/** @return Returns the last segment of the path */
102131
public String getLastSegment() {
103132
return segments.get(length() - 1);

0 commit comments

Comments
 (0)