firebase / firebase-android-sdk

Firebase Android SDK
https://firebase.google.com
Apache License 2.0
2.24k stars 568 forks source link

`Query.startAfter(Double value)` returns one more document than expected. #3597

Open russellwheatley opened 2 years ago

russellwheatley commented 2 years ago

[READ] Step 1: Are you in the right place?

Issues filed here should be about bugs in the code in this repository. If you have a general question, need help debugging, or fall into some other category use one of these other channels:

[REQUIRED] Step 2: Describe your environment

[REQUIRED] Step 3: Describe the problem

Querying the database using the startAfter(double value) API is not returning the correct amount of documents (specifically returning one more than it should).

Steps to reproduce:

  1. Run flutter create testProject.
  2. Update "lib/main.dart" with the code under the "Relevant Code" header & input your firebase options as described in the code comment.
  3. Run flutter app on an android device, the logs will output:
    {search: 3}
    {search: 4}
    {search: 5}
  4. Run flutter app on an iOS device will produce the correct output:
    {search: 4}
    {search: 5}

The relevant android code where we call startAfter is here

Relevant Code:

import 'dart:async';

import 'package:firebase_core/firebase_core.dart';
import 'package:firebase_database/firebase_database.dart';
import 'package:flutter/material.dart';

import 'firebase_options.dart';

Future<void> main() async {
  WidgetsFlutterBinding.ensureInitialized();
  await Firebase.initializeApp(

    options: FirebaseOptions(
        // <INSERT YOUR OWN FIREBASE OPTIONS TAKEN FROM "google-services.json" FILE>        
    ),
  );

  runApp(
     MaterialApp(
      title: 'Flutter Database Example',
      home: MyHomePage(),
    ),
  );
}

class MyHomePage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('Flutter Database Example'),
      ),
      body: Column(
        children: [
          ElevatedButton(
            onPressed: () async {
                //CREATES 5 DOCUMENTS IN DATABASE
              DatabaseReference _testsRef = FirebaseDatabase.instance.ref('tests');
              await _testsRef.set({
                'yo':{'search':2,},
                'so':{'search':3,},
                'to':{'search':4,},
                'fu':{'search':1,},
                'bo':{'search':5,},
              });
            // QUERY THAT SHOULD RETURN 2 DOCUMENTS IF STARTING AFTER 3 (THE THIRD DOCUMENT)
             DataSnapshot response = await _testsRef.orderByChild('search').startAfter(3).get();

             response.children.forEach((element) {
                // PRINTS OUT THE DOCUMENTS RETURNED
               print(element.value.toString());
             });
            },
            child: const Text('Press me'),
          ),
        ],
      ),
    );
  }
}

Hopefully, this makes sense as a flutter implementation! It is very straight forward. In a nutshell;

  1. Create 5 documents.
  2. Use orderByChild(String path) & startAfter(Double value) to query documents, and note that the query returns one more document than expected.

Let me know if you need any further information.

argzdev commented 2 years ago

Hi @russellwheatley, thanks for reporting. Unfortunately, I'm unable to reproduce the issue when I created a minimal repro of your issue with native Android. I was able to get the same results of what should happen on an iOS device.

Logs:

D/MainActivity: Update complete
D/MainActivity: test: DataSnapshot { key = tests, value = {to={search=4}, bo={search=5}} }
D/MainActivity: test: DataSnapshot { key = bo, value = {search=5} }
D/MainActivity: test: DataSnapshot { key = to, value = {search=4} }

Relevant code:

fun test(view: View){
        val database = Firebase.database
        val testRef = database.getReference("tests")

        testRef.setValue(mapOf(
            "yo" to mapOf("search" to 2),
            "so" to mapOf("search" to 3),
            "to" to mapOf("search" to 4),
            "fu" to mapOf("search" to 1),
            "bo" to mapOf("search" to 5),
        ))

        Log.d(TAG, "Update complete")

        testRef.orderByChild("search").startAfter((3).toDouble()).get().addOnSuccessListener { snapshot ->
            Log.d(TAG, "test: $snapshot")
            snapshot.children.forEach { child ->
                Log.d(TAG, "test: $child")
            }
        }
        .addOnFailureListener{
            Log.d(TAG, "exception: $it")
        }
    }

I did notice in the startAfter a double parameter is required, I'm not sure if this can somehow impact the query, but just something to take note of.

This issue may be more of within the Flutter sdk itself. Could you provide more details about the issue so we can investigate this further?

Also, may I ask what's the equivalent Android version of Firebase RTDB of Flutter being used? Thanks in advance!

russellwheatley commented 2 years ago

Hey @argzdev, I've just recreated this issue using java:

        DatabaseReference testRef = FirebaseDatabase.getInstance().getReference("tests");
        String TAG = "TTTTTTTTTT";
        Map<String, Number> ob1 = new HashMap();
        Map<String, Number> ob2 = new HashMap();
        Map<String, Number> ob3 = new HashMap();
        Map<String, Number> ob4 = new HashMap();
        Map<String, Number> ob5 = new HashMap();

        ob1.put("search", 1);
        ob2.put("search", 3);
        ob3.put("search", 2);
        ob4.put("search", 5);
        ob5.put("search", 4);

        Map<String, Map<String, Number>> oby = new HashMap();

        oby.put("yo", ob1);
        oby.put("to", ob2);
        oby.put("lo", ob3);
        oby.put("fo", ob4);
        oby.put("eo", ob5);

        Tasks.await(testRef.setValue(oby));

        Log.d(TAG, "Update complete");

        Number dub = 3;
        testRef.orderByChild("search").startAfter(dub.doubleValue()).get().addOnCompleteListener(new OnCompleteListener<DataSnapshot>() {
          @Override
          public void onComplete(@NonNull Task<DataSnapshot> task) {
            if (!task.isSuccessful()) {
              Log.e(TAG, "Error getting data", task.getException());
            } else {
              Map<String, Map<String, Number>> result = (Map<String, Map<String, Number>>) task.getResult().getValue();
              Log.d(TAG, "onComplete: " + result.toString());
            }
          }
        });

and I got the following log:

D/TTTTTTTTTT(25687): onComplete: {fo={search=5}, eo={search=4}, to={search=3}}

We are using the latest Firebase BOM (29.3.0). Not sure what is different 😅

argzdev commented 2 years ago

Hi @russellwheatley, thanks for the repro. I was able to replicate the issue.

It turns out there was a race condition which when I was uploading data and then the search was happening is somewhat causing issues on the results.

I'm now able to reproduce the issue on both Kotlin and Java. I'll notify an engineer to take a look at this. Thanks!

TarekkMA commented 3 months ago

Any updates on this issue?