diff --git a/google-cloud-firestore/src/main/java/com/google/cloud/firestore/BasePath.java b/google-cloud-firestore/src/main/java/com/google/cloud/firestore/BasePath.java index 448c6f320..11c4110c8 100644 --- a/google-cloud-firestore/src/main/java/com/google/cloud/firestore/BasePath.java +++ b/google-cloud-firestore/src/main/java/com/google/cloud/firestore/BasePath.java @@ -149,7 +149,7 @@ private int compareSegments(String lhs, String rhs) { } else if (isLhsNumeric && isRhsNumeric) { // both numeric return Long.compare(extractNumericId(lhs), extractNumericId(rhs)); } else { // both string - return lhs.compareTo(rhs); + return Order.compareUtf8Strings(lhs, rhs); } } diff --git a/google-cloud-firestore/src/main/java/com/google/cloud/firestore/Order.java b/google-cloud-firestore/src/main/java/com/google/cloud/firestore/Order.java index 28434920a..4cd58c95b 100644 --- a/google-cloud-firestore/src/main/java/com/google/cloud/firestore/Order.java +++ b/google-cloud-firestore/src/main/java/com/google/cloud/firestore/Order.java @@ -115,7 +115,7 @@ public int compare(@Nonnull Value left, @Nonnull Value right) { case TIMESTAMP: return compareTimestamps(left, right); case STRING: - return compareStrings(left, right); + return compareUtf8Strings(left.getStringValue(), right.getStringValue()); case BLOB: return compareBlobs(left, right); case REF: @@ -134,14 +134,20 @@ public int compare(@Nonnull Value left, @Nonnull Value right) { } } - private int compareStrings(Value left, Value right) { - return left.getStringValue().compareTo(right.getStringValue()); + /** Compare strings in UTF-8 encoded byte order */ + public static int compareUtf8Strings(String left, String right) { + ByteString leftBytes = ByteString.copyFromUtf8(left); + ByteString rightBytes = ByteString.copyFromUtf8(right); + return compareByteStrings(leftBytes, rightBytes); } private int compareBlobs(Value left, Value right) { ByteString leftBytes = left.getBytesValue(); ByteString rightBytes = right.getBytesValue(); + return compareByteStrings(leftBytes, rightBytes); + } + private static int compareByteStrings(ByteString leftBytes, ByteString rightBytes) { int size = Math.min(leftBytes.size(), rightBytes.size()); for (int i = 0; i < size; i++) { // Make sure the bytes are unsigned @@ -211,7 +217,7 @@ private int compareObjects(Value left, Value right) { while (leftIterator.hasNext() && rightIterator.hasNext()) { Entry leftEntry = leftIterator.next(); Entry rightEntry = rightIterator.next(); - int keyCompare = leftEntry.getKey().compareTo(rightEntry.getKey()); + int keyCompare = compareUtf8Strings(leftEntry.getKey(), rightEntry.getKey()); if (keyCompare != 0) { return keyCompare; } diff --git a/google-cloud-firestore/src/test/java/com/google/cloud/firestore/it/ITQueryTest.java b/google-cloud-firestore/src/test/java/com/google/cloud/firestore/it/ITQueryTest.java index 2381d0439..5ffb80d2a 100644 --- a/google-cloud-firestore/src/test/java/com/google/cloud/firestore/it/ITQueryTest.java +++ b/google-cloud-firestore/src/test/java/com/google/cloud/firestore/it/ITQueryTest.java @@ -1155,4 +1155,205 @@ public void snapshotListenerSortsNumbersSameWayAsServer() throws Exception { assertEquals(queryOrder, listenerOrder); // Assert order in the SDK } + + @Test + public void snapshotListenerSortsUnicodeStringsSameWayAsServer() throws Exception { + CollectionReference col = createEmptyCollection(); + + firestore + .batch() + .set(col.document("a"), map("value", "Łukasiewicz")) + .set(col.document("b"), map("value", "Sierpiński")) + .set(col.document("c"), map("value", "岩澤")) + .set(col.document("d"), map("value", "🄟")) + .set(col.document("e"), map("value", "P")) + .set(col.document("f"), map("value", "︒")) + .set(col.document("g"), map("value", "🐵")) + .commit() + .get(); + + Query query = col.orderBy("value", Direction.ASCENDING); + + QuerySnapshot snapshot = query.get().get(); + List queryOrder = + snapshot.getDocuments().stream().map(doc -> doc.getId()).collect(Collectors.toList()); + + CountDownLatch latch = new CountDownLatch(1); + List 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(queryOrder, Arrays.asList("b", "a", "c", "f", "e", "d", "g")); + assertEquals(queryOrder, listenerOrder); + } + + @Test + public void snapshotListenerSortsUnicodeStringsInArraySameWayAsServer() throws Exception { + CollectionReference col = createEmptyCollection(); + + firestore + .batch() + .set(col.document("a"), map("value", Arrays.asList("Łukasiewicz"))) + .set(col.document("b"), map("value", Arrays.asList("Sierpiński"))) + .set(col.document("c"), map("value", Arrays.asList("岩澤"))) + .set(col.document("d"), map("value", Arrays.asList("🄟"))) + .set(col.document("e"), map("value", Arrays.asList("P"))) + .set(col.document("f"), map("value", Arrays.asList("︒"))) + .set(col.document("g"), map("value", Arrays.asList("🐵"))) + .commit() + .get(); + + Query query = col.orderBy("value", Direction.ASCENDING); + + QuerySnapshot snapshot = query.get().get(); + List queryOrder = + snapshot.getDocuments().stream().map(doc -> doc.getId()).collect(Collectors.toList()); + + CountDownLatch latch = new CountDownLatch(1); + List 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(queryOrder, Arrays.asList("b", "a", "c", "f", "e", "d", "g")); + assertEquals(queryOrder, listenerOrder); + } + + @Test + public void snapshotListenerSortsUnicodeStringsInMapSameWayAsServer() throws Exception { + CollectionReference col = createEmptyCollection(); + + firestore + .batch() + .set(col.document("a"), map("value", map("foo", "Łukasiewicz"))) + .set(col.document("b"), map("value", map("foo", "Sierpiński"))) + .set(col.document("c"), map("value", map("foo", "岩澤"))) + .set(col.document("d"), map("value", map("foo", "🄟"))) + .set(col.document("e"), map("value", map("foo", "P"))) + .set(col.document("f"), map("value", map("foo", "︒"))) + .set(col.document("g"), map("value", map("foo", "🐵"))) + .commit() + .get(); + + Query query = col.orderBy("value", Direction.ASCENDING); + + QuerySnapshot snapshot = query.get().get(); + List queryOrder = + snapshot.getDocuments().stream().map(doc -> doc.getId()).collect(Collectors.toList()); + + CountDownLatch latch = new CountDownLatch(1); + List 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(queryOrder, Arrays.asList("b", "a", "c", "f", "e", "d", "g")); + assertEquals(queryOrder, listenerOrder); + } + + @Test + public void snapshotListenerSortsUnicodeStringsInMapKeySameWayAsServer() throws Exception { + CollectionReference col = createEmptyCollection(); + + firestore + .batch() + .set(col.document("a"), map("value", map("Łukasiewicz", "foo"))) + .set(col.document("b"), map("value", map("Sierpiński", "foo"))) + .set(col.document("c"), map("value", map("岩澤", "foo"))) + .set(col.document("d"), map("value", map("🄟", "foo"))) + .set(col.document("e"), map("value", map("P", "foo"))) + .set(col.document("f"), map("value", map("︒", "foo"))) + .set(col.document("g"), map("value", map("🐵", "foo"))) + .commit() + .get(); + + Query query = col.orderBy("value", Direction.ASCENDING); + + QuerySnapshot snapshot = query.get().get(); + List queryOrder = + snapshot.getDocuments().stream().map(doc -> doc.getId()).collect(Collectors.toList()); + + CountDownLatch latch = new CountDownLatch(1); + List 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(queryOrder, Arrays.asList("b", "a", "c", "f", "e", "d", "g")); + assertEquals(queryOrder, listenerOrder); + } + + @Test + public void snapshotListenerSortsUnicodeStringsInDocumentKeySameWayAsServer() throws Exception { + CollectionReference col = createEmptyCollection(); + + firestore + .batch() + .set(col.document("Łukasiewicz"), map("value", "foo")) + .set(col.document("Sierpiński"), map("value", "foo")) + .set(col.document("岩澤"), map("value", "foo")) + .set(col.document("🄟"), map("value", "foo")) + .set(col.document("P"), map("value", "foo")) + .set(col.document("︒"), map("value", "foo")) + .set(col.document("🐵"), map("value", "foo")) + .commit() + .get(); + + Query query = col.orderBy(FieldPath.documentId()); + + QuerySnapshot snapshot = query.get().get(); + List queryOrder = + snapshot.getDocuments().stream().map(doc -> doc.getId()).collect(Collectors.toList()); + + CountDownLatch latch = new CountDownLatch(1); + List 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( + queryOrder, Arrays.asList("Sierpiński", "Łukasiewicz", "岩澤", "︒", "P", "🄟", "🐵")); + assertEquals(queryOrder, listenerOrder); + } }