androidfirebasekotlingoogle-cloud-firestorepagination

Android Firestore query paging FirebaseFirestoreException: INVALID_ARGUMENT


I have a function that fetches posts from my firestore db and I am following the docs for paging a query by last index document but for some reason after first fetch when trying to run the next query I receive com.google.firebase.firestore.FirebaseFirestoreException: INVALID_ARGUMENT: Order by clause cannot contain duplicate fields uploadTimeStamp

I also logged and saw that the query seems ok and the lastVisible is actually index 4 in the documents list after running first query, but somehow in the second query I receive all first 5 posts again..

any idea what I might be doing wrong?


suspend fun getMyPosts(userID: String): Flow<MutableList<DocumentSnapshot>> = flow {
        Log.d(TAG, "getMyPosts() called -> userID is $userID")
        var data: QuerySnapshot? = null
        try {
            data = dbRef.collection("posts")
                .whereEqualTo("userID", userID)
                .orderBy("uploadTimeStamp", Query.Direction.DESCENDING)
                .limit(5)
                .get()
                .await()
            Log.d(TAG, "emitting ${data.documents.size} posts: \n")
            data.documents.forEach { doc ->
                Log.d(
                    TAG,
                    "post number ${data.documents.indexOf(doc)}, timestamp: ${doc.get("uploadTimeStamp")}"
                )
            }

            emit(data.documents)
            while (!data!!.isEmpty && data.size() == 5) {
                Log.d(TAG, "getting more posts")
                val lastVisible = data.documents[data.size() - 1]
                Log.d(TAG, "last index is ${data.size() - 1}")
                Log.d(TAG, "lastVisible is ${lastVisible.get("uploadTimeStamp")}")
                data = data.query
                    .whereEqualTo("userID", userID)
                    .orderBy("uploadTimeStamp", Query.Direction.DESCENDING)
                    .startAfter(lastVisible)
                    .limit(5)
                    .get()
                    .await()
                if (!data.isEmpty) {
                    Log.d(TAG, "emitting ${data.documents}")
                    emit(data.documents)
                }
            }
        } catch (e: Exception) {
            Log.d(TAG, "Failed to get MyPosts,\n $e")
            data!!.documents.forEach { doc ->
                Log.d(
                    TAG,
                    "post number ${data.documents.indexOf(doc)}, timestamp: ${doc.get("uploadTimeStamp")}"
                )
            }
        }
    }

here is the logcat when i run getMyPosts() so you can see that I somehow get the first 5 docs again:


2021-06-03 12:31:33.349 4990-5249/avedot.app D/AppRepository: getMyPosts() called -> userID is ncTe77npPHPJyQvJJdRUAY1fEBB3
2021-06-03 12:31:34.048 4990-5249/avedot.app D/AppRepository: emitting 5 posts: 
2021-06-03 12:31:34.048 4990-5249/avedot.app D/AppRepository: post number 0, timestamp: 1622708658810
2021-06-03 12:31:34.048 4990-5249/avedot.app D/AppRepository: post number 1, timestamp: 1622708316148
2021-06-03 12:31:34.049 4990-5249/avedot.app D/AppRepository: post number 2, timestamp: 1622665878322
2021-06-03 12:31:34.049 4990-5249/avedot.app D/AppRepository: post number 3, timestamp: 1622665769466
2021-06-03 12:31:34.049 4990-5249/avedot.app D/AppRepository: post number 4, timestamp: 1622661053130
2021-06-03 12:31:34.082 4990-5249/avedot.app D/AppRepository: getImages() called
2021-06-03 12:31:34.083 4990-5249/avedot.app D/AppRepository: [false, false, true]
2021-06-03 12:31:35.345 4990-5249/avedot.app D/AppRepository: [false, true, true]
2021-06-03 12:31:36.068 4990-5249/avedot.app D/AppRepository: [true, true, true]
2021-06-03 12:31:37.084 4990-5249/avedot.app D/AppRepository: [false, false, true]
2021-06-03 12:31:37.294 4990-5249/avedot.app D/AppRepository: [false, false, true]
2021-06-03 12:31:37.500 4990-5249/avedot.app D/AppRepository: getting more posts
2021-06-03 12:31:37.500 4990-5249/avedot.app D/AppRepository: last index is 4
2021-06-03 12:31:37.501 4990-5249/avedot.app D/AppRepository: lastVisible is 1622661053130
2021-06-03 12:31:37.666 4990-5249/avedot.app D/AppRepository: Failed to get MyPosts,
     com.google.firebase.firestore.FirebaseFirestoreException: INVALID_ARGUMENT: Order by clause cannot contain duplicate fields uploadTimeStamp
2021-06-03 12:31:37.666 4990-5249/avedot.app D/AppRepository: post number 0, timestamp: 1622708658810
2021-06-03 12:31:37.666 4990-5249/avedot.app D/AppRepository: post number 1, timestamp: 1622708316148
2021-06-03 12:31:37.667 4990-5249/avedot.app D/AppRepository: post number 2, timestamp: 1622665878322
2021-06-03 12:31:37.667 4990-5249/avedot.app D/AppRepository: post number 3, timestamp: 1622665769466
2021-06-03 12:31:37.667 4990-5249/avedot.app D/AppRepository: post number 4, timestamp: 1622661053130

this is my firestore document structure


Solution

  • If we simplify your code a bit, you're doing:

    var data: QuerySnapshot? = null
    try {
        data = dbRef.collection("posts")
            .whereEqualTo("userID", userID)
            .orderBy("uploadTimeStamp", Query.Direction.DESCENDING)
            .limit(5)
            .get()
            .await()
        ...
        while (!data!!.isEmpty && data.size() == 5) {
            val lastVisible = data.documents[data.size() - 1]
            data = data.query
                .whereEqualTo("userID", userID)
                .orderBy("uploadTimeStamp", Query.Direction.DESCENDING)
                .startAfter(lastVisible)
                .limit(5)
                .get()
                .await()
    

    So you're building data out from its original value, by calling whereEqualTo and orderBy again on it, which is what the error message is complaining about.

    The simplest fix is to not re-use the existing value of data, so to change data = data.query to data = dbRef.collection("posts").

    But you seem to be trying a builder pattern, which is actually a better approach. You'll need to make sure in that case that you're not rebuilding the entire query, but only add the clauses that are relevant to the pagination section:

    data = data.query
        .startAfter(lastVisible)
        .get()
        .await()
    

    So here we're just adding the startAfter clause, as all other conditions were already present in the data.query as you first built it.