diff --git a/packages/firestore/src/api/database.ts b/packages/firestore/src/api/database.ts index 0192f46cd1d..52b51452a66 100644 --- a/packages/firestore/src/api/database.ts +++ b/packages/firestore/src/api/database.ts @@ -46,7 +46,8 @@ import { FieldValue, FieldValueOptions, ObjectValue, - RefValue + RefValue, + ServerTimestampValue } from '../model/field_value'; import { DeleteMutation, Mutation, Precondition } from '../model/mutation'; import { FieldPath, ResourcePath } from '../model/path'; @@ -1557,7 +1558,8 @@ export class Query implements firestore.Query { * position. * * Will throw if the document does not contain all fields of the order by - * of the query. + * of the query or if any of the fields in the order by are an uncommitted + * server timestamp. */ private boundFromDocument( methodName: string, @@ -1578,7 +1580,16 @@ export class Query implements firestore.Query { components.push(new RefValue(this.firestore._databaseId, doc.key)); } else { const value = doc.field(orderBy.field); - if (value !== undefined) { + if (value instanceof ServerTimestampValue) { + throw new FirestoreError( + Code.INVALID_ARGUMENT, + 'Invalid query. You are trying to start or end a query using a ' + + 'document for which the field "' + + orderBy.field + + '" is an uncommitted server timestamp. (Since the value of ' + + 'this field is unknown, you cannot start/end a query with it.)' + ); + } else if (value !== undefined) { components.push(value); } else { const field = orderBy.field.canonicalString(); diff --git a/packages/firestore/test/integration/api/validation.test.ts b/packages/firestore/test/integration/api/validation.test.ts index a66084c395c..24078be79df 100644 --- a/packages/firestore/test/integration/api/validation.test.ts +++ b/packages/firestore/test/integration/api/validation.test.ts @@ -19,6 +19,7 @@ import * as firestore from '@firebase/firestore-types'; import { expect } from 'chai'; import { CACHE_SIZE_UNLIMITED } from '../../../src/api/database'; +import { Deferred } from '../../util/promise'; import firebase from '../util/firebase_export'; import { ALT_PROJECT_ID, @@ -795,6 +796,60 @@ apiDescribe('Validation:', persistence => { }); }); + validationIt( + persistence, + 'cannot be sorted by an uncommitted server timestamp', + db => { + return withTestCollection( + persistence, + /*docs=*/ {}, + async (collection: firestore.CollectionReference) => { + await db.disableNetwork(); + + const offlineDeferred = new Deferred(); + const onlineDeferred = new Deferred(); + + const unsubscribe = collection.onSnapshot(snapshot => { + // Skip the initial empty snapshot. + if (snapshot.empty) return; + + expect(snapshot.docs).to.have.lengthOf(1); + const docSnap: firestore.DocumentSnapshot = snapshot.docs[0]; + + if (snapshot.metadata.hasPendingWrites) { + // Offline snapshot. Since the server timestamp is uncommitted, + // we shouldn't be able to query by it. + expect(() => + collection + .orderBy('timestamp') + .endAt(docSnap) + .onSnapshot(() => {}) + ).to.throw('uncommitted server timestamp'); + offlineDeferred.resolve(); + } else { + // Online snapshot. Since the server timestamp is committed, we + // should be able to query by it. + collection + .orderBy('timestamp') + .endAt(docSnap) + .onSnapshot(() => {}); + onlineDeferred.resolve(); + } + }); + + const doc: firestore.DocumentReference = collection.doc(); + doc.set({ timestamp: FieldValue.serverTimestamp() }); + await offlineDeferred.promise; + + await db.enableNetwork(); + await onlineDeferred.promise; + + unsubscribe(); + } + ); + } + ); + validationIt( persistence, 'must not have more components than order by.',