Skip to content

Commit a1cbf93

Browse files
authored
FIX: sort strings in UTF-8 encoded byte order (#6615)
1 parent b5dbd0a commit a1cbf93

File tree

5 files changed

+217
-4
lines changed

5 files changed

+217
-4
lines changed

firebase-firestore/CHANGELOG.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
# Unreleased
2-
2+
* [fixed] Fixed a server and sdk mismatch in unicode string sorting. [#6615](//github.com/firebase/firebase-android-sdk/pull/6615)
33

44
# 25.1.1
55
* [changed] Update Firestore proto definitions. [#6369](//github.com/firebase/firebase-android-sdk/pull/6369)

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

Lines changed: 206 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1653,4 +1653,210 @@ public void sdkOrdersQueryByDocumentIdTheSameWayOnlineAndOffline() {
16531653
// Run query with snapshot listener
16541654
checkOnlineAndOfflineResultsMatch(orderedQuery, expectedDocIds.toArray(new String[0]));
16551655
}
1656+
1657+
@Test
1658+
public void snapshotListenerSortsUnicodeStringsAsServer() {
1659+
Map<String, Map<String, Object>> testDocs =
1660+
map(
1661+
"a", map("value", "Łukasiewicz"),
1662+
"b", map("value", "Sierpiński"),
1663+
"c", map("value", "岩澤"),
1664+
"d", map("value", "🄟"),
1665+
"e", map("value", "P"),
1666+
"f", map("value", "︒"),
1667+
"g", map("value", "🐵"));
1668+
1669+
CollectionReference colRef = testCollectionWithDocs(testDocs);
1670+
Query orderedQuery = colRef.orderBy("value");
1671+
List<String> expectedDocIds = Arrays.asList("b", "a", "c", "f", "e", "d", "g");
1672+
1673+
QuerySnapshot getSnapshot = waitFor(orderedQuery.get());
1674+
List<String> getSnapshotDocIds =
1675+
getSnapshot.getDocuments().stream().map(ds -> ds.getId()).collect(Collectors.toList());
1676+
1677+
EventAccumulator<QuerySnapshot> eventAccumulator = new EventAccumulator<QuerySnapshot>();
1678+
ListenerRegistration registration =
1679+
orderedQuery.addSnapshotListener(eventAccumulator.listener());
1680+
1681+
List<String> watchSnapshotDocIds = new ArrayList<>();
1682+
try {
1683+
QuerySnapshot watchSnapshot = eventAccumulator.await();
1684+
watchSnapshotDocIds =
1685+
watchSnapshot.getDocuments().stream()
1686+
.map(documentSnapshot -> documentSnapshot.getId())
1687+
.collect(Collectors.toList());
1688+
} finally {
1689+
registration.remove();
1690+
}
1691+
1692+
assertTrue(getSnapshotDocIds.equals(expectedDocIds));
1693+
assertTrue(watchSnapshotDocIds.equals(expectedDocIds));
1694+
1695+
checkOnlineAndOfflineResultsMatch(orderedQuery, expectedDocIds.toArray(new String[0]));
1696+
}
1697+
1698+
@Test
1699+
public void snapshotListenerSortsUnicodeStringsInArrayAsServer() {
1700+
Map<String, Map<String, Object>> testDocs =
1701+
map(
1702+
"a", map("value", Arrays.asList("Łukasiewicz")),
1703+
"b", map("value", Arrays.asList("Sierpiński")),
1704+
"c", map("value", Arrays.asList("岩澤")),
1705+
"d", map("value", Arrays.asList("🄟")),
1706+
"e", map("value", Arrays.asList("P")),
1707+
"f", map("value", Arrays.asList("︒")),
1708+
"g", map("value", Arrays.asList("🐵")));
1709+
1710+
CollectionReference colRef = testCollectionWithDocs(testDocs);
1711+
Query orderedQuery = colRef.orderBy("value");
1712+
List<String> expectedDocIds = Arrays.asList("b", "a", "c", "f", "e", "d", "g");
1713+
1714+
QuerySnapshot getSnapshot = waitFor(orderedQuery.get());
1715+
List<String> getSnapshotDocIds =
1716+
getSnapshot.getDocuments().stream().map(ds -> ds.getId()).collect(Collectors.toList());
1717+
1718+
EventAccumulator<QuerySnapshot> eventAccumulator = new EventAccumulator<QuerySnapshot>();
1719+
ListenerRegistration registration =
1720+
orderedQuery.addSnapshotListener(eventAccumulator.listener());
1721+
1722+
List<String> watchSnapshotDocIds = new ArrayList<>();
1723+
try {
1724+
QuerySnapshot watchSnapshot = eventAccumulator.await();
1725+
watchSnapshotDocIds =
1726+
watchSnapshot.getDocuments().stream()
1727+
.map(documentSnapshot -> documentSnapshot.getId())
1728+
.collect(Collectors.toList());
1729+
} finally {
1730+
registration.remove();
1731+
}
1732+
1733+
assertTrue(getSnapshotDocIds.equals(expectedDocIds));
1734+
assertTrue(watchSnapshotDocIds.equals(expectedDocIds));
1735+
1736+
checkOnlineAndOfflineResultsMatch(orderedQuery, expectedDocIds.toArray(new String[0]));
1737+
}
1738+
1739+
@Test
1740+
public void snapshotListenerSortsUnicodeStringsInMapAsServer() {
1741+
Map<String, Map<String, Object>> testDocs =
1742+
map(
1743+
"a", map("value", map("foo", "Łukasiewicz")),
1744+
"b", map("value", map("foo", "Sierpiński")),
1745+
"c", map("value", map("foo", "岩澤")),
1746+
"d", map("value", map("foo", "🄟")),
1747+
"e", map("value", map("foo", "P")),
1748+
"f", map("value", map("foo", "︒")),
1749+
"g", map("value", map("foo", "🐵")));
1750+
1751+
CollectionReference colRef = testCollectionWithDocs(testDocs);
1752+
Query orderedQuery = colRef.orderBy("value");
1753+
List<String> expectedDocIds = Arrays.asList("b", "a", "c", "f", "e", "d", "g");
1754+
1755+
QuerySnapshot getSnapshot = waitFor(orderedQuery.get());
1756+
List<String> getSnapshotDocIds =
1757+
getSnapshot.getDocuments().stream().map(ds -> ds.getId()).collect(Collectors.toList());
1758+
1759+
EventAccumulator<QuerySnapshot> eventAccumulator = new EventAccumulator<QuerySnapshot>();
1760+
ListenerRegistration registration =
1761+
orderedQuery.addSnapshotListener(eventAccumulator.listener());
1762+
1763+
List<String> watchSnapshotDocIds = new ArrayList<>();
1764+
try {
1765+
QuerySnapshot watchSnapshot = eventAccumulator.await();
1766+
watchSnapshotDocIds =
1767+
watchSnapshot.getDocuments().stream()
1768+
.map(documentSnapshot -> documentSnapshot.getId())
1769+
.collect(Collectors.toList());
1770+
} finally {
1771+
registration.remove();
1772+
}
1773+
1774+
assertTrue(getSnapshotDocIds.equals(expectedDocIds));
1775+
assertTrue(watchSnapshotDocIds.equals(expectedDocIds));
1776+
1777+
checkOnlineAndOfflineResultsMatch(orderedQuery, expectedDocIds.toArray(new String[0]));
1778+
}
1779+
1780+
@Test
1781+
public void snapshotListenerSortsUnicodeStringsInMapKeyAsServer() {
1782+
Map<String, Map<String, Object>> testDocs =
1783+
map(
1784+
"a", map("value", map("Łukasiewicz", "foo")),
1785+
"b", map("value", map("Sierpiński", "foo")),
1786+
"c", map("value", map("岩澤", "foo")),
1787+
"d", map("value", map("🄟", "foo")),
1788+
"e", map("value", map("P", "foo")),
1789+
"f", map("value", map("︒", "foo")),
1790+
"g", map("value", map("🐵", "foo")));
1791+
1792+
CollectionReference colRef = testCollectionWithDocs(testDocs);
1793+
Query orderedQuery = colRef.orderBy("value");
1794+
List<String> expectedDocIds = Arrays.asList("b", "a", "c", "f", "e", "d", "g");
1795+
1796+
QuerySnapshot getSnapshot = waitFor(orderedQuery.get());
1797+
List<String> getSnapshotDocIds =
1798+
getSnapshot.getDocuments().stream().map(ds -> ds.getId()).collect(Collectors.toList());
1799+
1800+
EventAccumulator<QuerySnapshot> eventAccumulator = new EventAccumulator<QuerySnapshot>();
1801+
ListenerRegistration registration =
1802+
orderedQuery.addSnapshotListener(eventAccumulator.listener());
1803+
1804+
List<String> watchSnapshotDocIds = new ArrayList<>();
1805+
try {
1806+
QuerySnapshot watchSnapshot = eventAccumulator.await();
1807+
watchSnapshotDocIds =
1808+
watchSnapshot.getDocuments().stream()
1809+
.map(documentSnapshot -> documentSnapshot.getId())
1810+
.collect(Collectors.toList());
1811+
} finally {
1812+
registration.remove();
1813+
}
1814+
1815+
assertTrue(getSnapshotDocIds.equals(expectedDocIds));
1816+
assertTrue(watchSnapshotDocIds.equals(expectedDocIds));
1817+
1818+
checkOnlineAndOfflineResultsMatch(orderedQuery, expectedDocIds.toArray(new String[0]));
1819+
}
1820+
1821+
@Test
1822+
public void snapshotListenerSortsUnicodeStringsInDocumentKeyAsServer() {
1823+
Map<String, Map<String, Object>> testDocs =
1824+
map(
1825+
"Łukasiewicz", map("value", "foo"),
1826+
"Sierpiński", map("value", "foo"),
1827+
"岩澤", map("value", "foo"),
1828+
"🄟", map("value", "foo"),
1829+
"P", map("value", "foo"),
1830+
"︒", map("value", "foo"),
1831+
"🐵", map("value", "foo"));
1832+
1833+
CollectionReference colRef = testCollectionWithDocs(testDocs);
1834+
Query orderedQuery = colRef.orderBy(FieldPath.documentId());
1835+
List<String> expectedDocIds =
1836+
Arrays.asList("Sierpiński", "Łukasiewicz", "岩澤", "︒", "P", "🄟", "🐵");
1837+
1838+
QuerySnapshot getSnapshot = waitFor(orderedQuery.get());
1839+
List<String> getSnapshotDocIds =
1840+
getSnapshot.getDocuments().stream().map(ds -> ds.getId()).collect(Collectors.toList());
1841+
1842+
EventAccumulator<QuerySnapshot> eventAccumulator = new EventAccumulator<QuerySnapshot>();
1843+
ListenerRegistration registration =
1844+
orderedQuery.addSnapshotListener(eventAccumulator.listener());
1845+
1846+
List<String> watchSnapshotDocIds = new ArrayList<>();
1847+
try {
1848+
QuerySnapshot watchSnapshot = eventAccumulator.await();
1849+
watchSnapshotDocIds =
1850+
watchSnapshot.getDocuments().stream()
1851+
.map(documentSnapshot -> documentSnapshot.getId())
1852+
.collect(Collectors.toList());
1853+
} finally {
1854+
registration.remove();
1855+
}
1856+
1857+
assertTrue(getSnapshotDocIds.equals(expectedDocIds));
1858+
assertTrue(watchSnapshotDocIds.equals(expectedDocIds));
1859+
1860+
checkOnlineAndOfflineResultsMatch(orderedQuery, expectedDocIds.toArray(new String[0]));
1861+
}
16561862
}

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -114,7 +114,7 @@ private static int compareSegments(String lhs, String rhs) {
114114
} else if (isLhsNumeric && isRhsNumeric) { // both numeric
115115
return Long.compare(extractNumericId(lhs), extractNumericId(rhs));
116116
} else { // both string
117-
return lhs.compareTo(rhs);
117+
return Util.compareUtf8Strings(lhs, rhs);
118118
}
119119
}
120120

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -230,7 +230,7 @@ public static int compare(Value left, Value right) {
230230
case TYPE_ORDER_SERVER_TIMESTAMP:
231231
return compareTimestamps(getLocalWriteTime(left), getLocalWriteTime(right));
232232
case TYPE_ORDER_STRING:
233-
return left.getStringValue().compareTo(right.getStringValue());
233+
return Util.compareUtf8Strings(left.getStringValue(), right.getStringValue());
234234
case TYPE_ORDER_BLOB:
235235
return Util.compareByteStrings(left.getBytesValue(), right.getBytesValue());
236236
case TYPE_ORDER_REFERENCE:
@@ -349,7 +349,7 @@ private static int compareMaps(MapValue left, MapValue right) {
349349
while (iterator1.hasNext() && iterator2.hasNext()) {
350350
Map.Entry<String, Value> entry1 = iterator1.next();
351351
Map.Entry<String, Value> entry2 = iterator2.next();
352-
int keyCompare = entry1.getKey().compareTo(entry2.getKey());
352+
int keyCompare = Util.compareUtf8Strings(entry1.getKey(), entry2.getKey());
353353
if (keyCompare != 0) {
354354
return keyCompare;
355355
}

firebase-firestore/src/main/java/com/google/firebase/firestore/util/Util.java

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,13 @@ public static int compareIntegers(int i1, int i2) {
8585
}
8686
}
8787

88+
/** Compare strings in UTF-8 encoded byte order */
89+
public static int compareUtf8Strings(String left, String right) {
90+
ByteString leftBytes = ByteString.copyFromUtf8(left);
91+
ByteString rightBytes = ByteString.copyFromUtf8(right);
92+
return compareByteStrings(leftBytes, rightBytes);
93+
}
94+
8895
/**
8996
* Utility function to compare longs. Note that we can't use Long.compare because it's only
9097
* available after Android 19.

0 commit comments

Comments
 (0)