Skip to content

Commit 4ae3f1b

Browse files
authored
Forbid queries endAt an uncommitted server timestamp. (#138)
1 parent 03c141e commit 4ae3f1b

File tree

2 files changed

+64
-2
lines changed

2 files changed

+64
-2
lines changed

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

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,8 @@
1414

1515
package com.google.firebase.firestore;
1616

17+
import static com.google.common.truth.Truth.assertThat;
18+
import static com.google.firebase.firestore.testutil.Assert.assertThrows;
1719
import static com.google.firebase.firestore.testutil.IntegrationTestUtil.testAlternateFirestore;
1820
import static com.google.firebase.firestore.testutil.IntegrationTestUtil.testCollection;
1921
import static com.google.firebase.firestore.testutil.IntegrationTestUtil.testCollectionWithDocs;
@@ -30,6 +32,7 @@
3032

3133
import android.support.test.InstrumentationRegistry;
3234
import android.support.test.runner.AndroidJUnit4;
35+
import com.google.android.gms.tasks.TaskCompletionSource;
3336
import com.google.firebase.FirebaseApp;
3437
import com.google.firebase.FirebaseOptions;
3538
import com.google.firebase.firestore.Transaction.Function;
@@ -438,6 +441,56 @@ public void queriesCannotBeCreatedFromDocumentsMissingSortValues() {
438441
expectError(() -> query.endAt(snapshot), reason);
439442
}
440443

444+
@Test
445+
public void queriesCannotBeSortedByAnUncommittedServerTimestamp() {
446+
CollectionReference collection = testCollection();
447+
448+
// Ensure the server timestamp stays uncommitted for the first half of the test
449+
waitFor(collection.firestore.getClient().disableNetwork());
450+
451+
TaskCompletionSource<Void> offlineCallbackDone = new TaskCompletionSource<>();
452+
TaskCompletionSource<Void> onlineCallbackDone = new TaskCompletionSource<>();
453+
454+
collection.addSnapshotListener(
455+
(snapshot, error) -> {
456+
assertNotNull(snapshot);
457+
458+
// Skip the initial empty snapshot.
459+
if (snapshot.isEmpty()) return;
460+
461+
assertThat(snapshot.getDocuments()).hasSize(1);
462+
DocumentSnapshot docSnap = snapshot.getDocuments().get(0);
463+
464+
if (snapshot.getMetadata().hasPendingWrites()) {
465+
// Offline snapshot. Since the server timestamp is uncommitted, we shouldn't be able to
466+
// query by it.
467+
assertThrows(
468+
IllegalArgumentException.class,
469+
() ->
470+
collection
471+
.orderBy("timestamp")
472+
.endAt(docSnap)
473+
.addSnapshotListener((snapshot2, error2) -> {}));
474+
offlineCallbackDone.setResult(null);
475+
} else {
476+
// Online snapshot. Since the server timestamp is committed, we should be able to query
477+
// by it.
478+
collection
479+
.orderBy("timestamp")
480+
.endAt(docSnap)
481+
.addSnapshotListener((snapshot2, error2) -> {});
482+
onlineCallbackDone.setResult(null);
483+
}
484+
});
485+
486+
DocumentReference document = collection.document();
487+
document.set(map("timestamp", FieldValue.serverTimestamp()));
488+
waitFor(offlineCallbackDone.getTask());
489+
490+
waitFor(collection.firestore.getClient().enableNetwork());
491+
waitFor(onlineCallbackDone.getTask());
492+
}
493+
441494
@Test
442495
public void queriesMustNotHaveMoreComponentsThanOrderBy() {
443496
CollectionReference collection = testCollection();

firebase-firestore/src/main/java/com/google/firebase/firestore/Query.java

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@
3939
import com.google.firebase.firestore.model.ResourcePath;
4040
import com.google.firebase.firestore.model.value.FieldValue;
4141
import com.google.firebase.firestore.model.value.ReferenceValue;
42+
import com.google.firebase.firestore.model.value.ServerTimestampValue;
4243
import com.google.firebase.firestore.util.ExecutorEventListener;
4344
import com.google.firebase.firestore.util.Executors;
4445
import com.google.firebase.firestore.util.ListenerRegistrationImpl;
@@ -582,7 +583,8 @@ public Query endAt(Object... fieldValues) {
582583
* <p>Note that the Bound will always include the key of the document and so only the provided
583584
* document will compare equal to the returned position.
584585
*
585-
* <p>Will throw if the document does not contain all fields of the order by of the query.
586+
* <p>Will throw if the document does not contain all fields of the order by of the query or if
587+
* any of the fields in the order by are an uncommitted server timestamp.
586588
*/
587589
private Bound boundFromDocumentSnapshot(
588590
String methodName, DocumentSnapshot snapshot, boolean before) {
@@ -606,7 +608,14 @@ private Bound boundFromDocumentSnapshot(
606608
components.add(ReferenceValue.valueOf(firestore.getDatabaseId(), document.getKey()));
607609
} else {
608610
FieldValue value = document.getField(orderBy.getField());
609-
if (value != null) {
611+
if (value instanceof ServerTimestampValue) {
612+
throw new IllegalArgumentException(
613+
"Invalid query. You are trying to start or end a query using a document for which "
614+
+ "the field '"
615+
+ orderBy.getField()
616+
+ "' is an uncommitted server timestamp. (Since the value of this field is "
617+
+ "unknown, you cannot start/end a query with it.)");
618+
} else if (value != null) {
610619
components.add(value);
611620
} else {
612621
throw new IllegalArgumentException(

0 commit comments

Comments
 (0)