Explorar o código

Performing IN expansion (#10410)

* Performing IN expansion

* Add tests

* change the implementation of GetDocumentsMatchingTarget
cherylEnkidu %!s(int64=3) %!d(string=hai) anos
pai
achega
d5198acd91

+ 2 - 3
Firestore/core/src/local/leveldb_index_manager.cc

@@ -536,14 +536,13 @@ IndexManager::IndexType LevelDbIndexManager::GetIndexType(
 
 absl::optional<std::vector<model::DocumentKey>>
 LevelDbIndexManager::GetDocumentsMatchingTarget(const core::Target& target) {
-  std::unordered_map<core::Target, model::FieldIndex> indexes;
+  std::vector<std::pair<core::Target, model::FieldIndex>> indexes;
   for (const auto& sub_target : GetSubTargets(target)) {
     auto index_opt = GetFieldIndex(sub_target);
     if (!index_opt.has_value()) {
       return absl::nullopt;
     }
-
-    indexes.insert({sub_target, index_opt.value()});
+    indexes.emplace_back(sub_target, index_opt.value());
   }
 
   std::vector<DocumentKey> result;

+ 39 - 1
Firestore/core/src/util/logic_utils.cc

@@ -17,9 +17,11 @@
 #include "Firestore/core/src/util/logic_utils.h"
 
 #include <utility>
+#include <vector>
 
 #include "Firestore/core/src/core/composite_filter.h"
 #include "Firestore/core/src/core/field_filter.h"
+#include "Firestore/core/src/nanopb/message.h"
 #include "Firestore/core/src/util/hard_assert.h"
 
 namespace firebase {
@@ -254,13 +256,49 @@ Filter LogicUtils::ComputeDistributedNormalForm(const core::Filter& filter) {
   return running_result;
 }
 
+Filter LogicUtils::ComputeInExpansion(const Filter& filter) {
+  AssertFieldFilterOrCompositeFilter(filter);
+
+  std::vector<Filter> expanded_filters;
+
+  if (filter.IsAFieldFilter()) {
+    if (filter.type() == Filter::Type::kInFilter) {
+      // We have reached a field filter with `in` operator.
+      FieldFilter in_filter(filter);
+      for (pb_size_t i = 0; i < in_filter.value().array_value.values_count;
+           ++i) {
+        expanded_filters.push_back(
+            FieldFilter::Create(in_filter.field(), FieldFilter::Operator::Equal,
+                                nanopb::MakeSharedMessage(
+                                    in_filter.value().array_value.values[i])));
+      }
+      return CompositeFilter::Create(std::move(expanded_filters),
+                                     CompositeFilter::Operator::Or);
+    } else {
+      // We have reached other kinds of field filters.
+      return filter;
+    }
+  }
+
+  // We have a composite filter.
+  CompositeFilter composite_filter(filter);
+  for (const auto& subfilter : composite_filter.filters()) {
+    expanded_filters.push_back(ComputeInExpansion(subfilter));
+  }
+  return CompositeFilter::Create(std::move(expanded_filters),
+                                 composite_filter.op());
+}
+
 std::vector<core::Filter> LogicUtils::GetDnfTerms(
     const core::CompositeFilter& filter) {
   if (filter.IsEmpty()) {
     return {};
   }
 
-  Filter result = ComputeDistributedNormalForm(filter);
+  // The `in` operator is a syntactic sugar over a disjunction of equalities.
+  // We should first replace such filters with equality filters before running
+  // the DNF transform.
+  Filter result = ComputeDistributedNormalForm(ComputeInExpansion(filter));
 
   HARD_ASSERT(
       IsDisjunctiveNormalForm(result),

+ 8 - 0
Firestore/core/src/util/logic_utils.h

@@ -90,6 +90,14 @@ class LogicUtils {
 
   static core::Filter ComputeDistributedNormalForm(const core::Filter& filter);
 
+  /**
+   * The `in` filter is only a syntactic sugar over a disjunction of equalities.
+   * For instance: `a in [1,2,3]` is in fact `a==1 || a==2 || a==3`. This method
+   * expands any `in` filter in the given input into a disjunction of equality
+   * filters and returns the expanded filter.
+   */
+  static core::Filter ComputeInExpansion(const core::Filter& filter);
+
  private:
   /**
    * Asserts that the given filter is a FieldFilter or CompositeFilter.

+ 276 - 52
Firestore/core/test/unit/local/leveldb_query_engine_test.cc

@@ -34,7 +34,6 @@ namespace firestore {
 namespace local {
 namespace {
 
-using model::DocumentMap;
 using model::DocumentSet;
 using model::SnapshotVersion;
 using testutil::AndFilters;
@@ -55,6 +54,15 @@ std::unique_ptr<Persistence> PersistenceFactory() {
   return LevelDbPersistenceForTesting();
 }
 
+model::DocumentMap DocumentMap(
+    const std::vector<model::MutableDocument>& docs) {
+  model::DocumentMap doc_map;
+  for (const auto& doc : docs) {
+    doc_map = doc_map.insert(doc.key(), doc);
+  }
+  return doc_map;
+}
+
 }  // namespace
 
 INSTANTIATE_TEST_SUITE_P(LevelDbQueryEngineTest,
@@ -81,11 +89,7 @@ TEST_F(LevelDbQueryEngineTest, CombinesIndexedWithNonIndexedResults) {
         MakeFieldIndex("coll", "foo", model::Segment::kAscending));
 
     AddDocuments({doc1, doc2});
-
-    DocumentMap doc_map;
-    doc_map = doc_map.insert(doc1.key(), doc1);
-    doc_map = doc_map.insert(doc2.key(), doc2);
-    index_manager_->UpdateIndexEntries(doc_map);
+    index_manager_->UpdateIndexEntries(DocumentMap({doc1, doc2}));
     index_manager_->UpdateCollectionGroup(
         "coll", model::IndexOffset::FromDocument(doc2));
 
@@ -114,11 +118,7 @@ TEST_F(LevelDbQueryEngineTest, UsesPartialIndexForLimitQueries) {
 
     index_manager_->AddFieldIndex(
         MakeFieldIndex("coll", "a", model::Segment::kAscending));
-
-    DocumentMap doc_map;
-    doc_map = doc_map.insert(doc1.key(), doc1);
-    doc_map = doc_map.insert(doc2.key(), doc2);
-    index_manager_->UpdateIndexEntries(doc_map);
+    index_manager_->UpdateIndexEntries(DocumentMap({doc1, doc2}));
     index_manager_->UpdateCollectionGroup(
         "coll", model::IndexOffset::FromDocument(doc5));
 
@@ -145,13 +145,7 @@ TEST_F(LevelDbQueryEngineTest, RefillsIndexedLimitQueries) {
 
     index_manager_->AddFieldIndex(
         MakeFieldIndex("coll", "a", model::Segment::kAscending));
-
-    DocumentMap doc_map;
-    doc_map = doc_map.insert(doc1.key(), doc1);
-    doc_map = doc_map.insert(doc2.key(), doc2);
-    doc_map = doc_map.insert(doc3.key(), doc3);
-    doc_map = doc_map.insert(doc4.key(), doc4);
-    index_manager_->UpdateIndexEntries(doc_map);
+    index_manager_->UpdateIndexEntries(DocumentMap({doc1, doc2, doc3, doc4}));
     index_manager_->UpdateCollectionGroup(
         "coll", model::IndexOffset::FromDocument(doc4));
 
@@ -185,14 +179,8 @@ TEST_F(LevelDbQueryEngineTest, CanPerformOrQueriesUsingIndexes1) {
         MakeFieldIndex("coll", "b", model::Segment::kAscending));
     index_manager_->AddFieldIndex(
         MakeFieldIndex("coll", "b", model::Segment::kDescending));
-
-    DocumentMap doc_map;
-    doc_map = doc_map.insert(doc1.key(), doc1);
-    doc_map = doc_map.insert(doc2.key(), doc2);
-    doc_map = doc_map.insert(doc3.key(), doc3);
-    doc_map = doc_map.insert(doc4.key(), doc4);
-    doc_map = doc_map.insert(doc5.key(), doc5);
-    index_manager_->UpdateIndexEntries(doc_map);
+    index_manager_->UpdateIndexEntries(
+        DocumentMap({doc1, doc2, doc3, doc4, doc5}));
     index_manager_->UpdateCollectionGroup(
         "coll", model::IndexOffset::FromDocument(doc5));
 
@@ -256,14 +244,8 @@ TEST_F(LevelDbQueryEngineTest, CanPerformOrQueriesUsingIndexes2) {
         MakeFieldIndex("coll", "b", model::Segment::kAscending));
     index_manager_->AddFieldIndex(
         MakeFieldIndex("coll", "b", model::Segment::kDescending));
-
-    DocumentMap doc_map;
-    doc_map = doc_map.insert(doc1.key(), doc1);
-    doc_map = doc_map.insert(doc2.key(), doc2);
-    doc_map = doc_map.insert(doc3.key(), doc3);
-    doc_map = doc_map.insert(doc4.key(), doc4);
-    doc_map = doc_map.insert(doc5.key(), doc5);
-    index_manager_->UpdateIndexEntries(doc_map);
+    index_manager_->UpdateIndexEntries(
+        DocumentMap({doc1, doc2, doc3, doc4, doc5}));
     index_manager_->UpdateCollectionGroup(
         "coll", model::IndexOffset::FromDocument(doc5));
 
@@ -340,15 +322,8 @@ TEST_F(LevelDbQueryEngineTest, OrQueryWithInAndNotInUsingIndexes) {
         MakeFieldIndex("coll", "b", model::Segment::kAscending));
     index_manager_->AddFieldIndex(
         MakeFieldIndex("coll", "b", model::Segment::kDescending));
-
-    DocumentMap doc_map;
-    doc_map = doc_map.insert(doc1.key(), doc1);
-    doc_map = doc_map.insert(doc2.key(), doc2);
-    doc_map = doc_map.insert(doc3.key(), doc3);
-    doc_map = doc_map.insert(doc4.key(), doc4);
-    doc_map = doc_map.insert(doc5.key(), doc5);
-    doc_map = doc_map.insert(doc6.key(), doc6);
-    index_manager_->UpdateIndexEntries(doc_map);
+    index_manager_->UpdateIndexEntries(
+        DocumentMap({doc1, doc2, doc3, doc4, doc5, doc6}));
     index_manager_->UpdateCollectionGroup(
         "coll", model::IndexOffset::FromDocument(doc6));
 
@@ -387,15 +362,8 @@ TEST_F(LevelDbQueryEngineTest, OrQueryWithArrayMembershipUsingIndexes) {
         MakeFieldIndex("coll", "a", model::Segment::kDescending));
     index_manager_->AddFieldIndex(
         MakeFieldIndex("coll", "b", model::Segment::kContains));
-
-    DocumentMap doc_map;
-    doc_map = doc_map.insert(doc1.key(), doc1);
-    doc_map = doc_map.insert(doc2.key(), doc2);
-    doc_map = doc_map.insert(doc3.key(), doc3);
-    doc_map = doc_map.insert(doc4.key(), doc4);
-    doc_map = doc_map.insert(doc5.key(), doc5);
-    doc_map = doc_map.insert(doc6.key(), doc6);
-    index_manager_->UpdateIndexEntries(doc_map);
+    index_manager_->UpdateIndexEntries(
+        DocumentMap({doc1, doc2, doc3, doc4, doc5, doc6}));
     index_manager_->UpdateCollectionGroup(
         "coll", model::IndexOffset::FromDocument(doc6));
 
@@ -414,6 +382,262 @@ TEST_F(LevelDbQueryEngineTest, OrQueryWithArrayMembershipUsingIndexes) {
   });
 }
 
+TEST_F(LevelDbQueryEngineTest, QueryWithMultipleInsOnTheSameField) {
+  persistence_->Run("QueryWithMultipleInsOnTheSameField", [&] {
+    mutation_queue_->Start();
+    index_manager_->Start();
+
+    auto doc1 = Doc("coll/1", 1, Map("a", 1, "b", 0));
+    auto doc2 = Doc("coll/2", 1, Map("b", 1));
+    auto doc3 = Doc("coll/3", 1, Map("a", 3, "b", 2));
+    auto doc4 = Doc("coll/4", 1, Map("a", 1, "b", 3));
+    auto doc5 = Doc("coll/5", 1, Map("a", 1));
+    auto doc6 = Doc("coll/6", 1, Map("a", 2));
+    AddDocuments({doc1, doc2, doc3, doc4, doc5, doc6});
+    index_manager_->AddFieldIndex(
+        MakeFieldIndex("coll", "a", model::Segment::kAscending));
+    index_manager_->AddFieldIndex(
+        MakeFieldIndex("coll", "a", model::Segment::kDescending));
+    index_manager_->AddFieldIndex(
+        MakeFieldIndex("coll", "b", model::Segment::kAscending));
+    index_manager_->AddFieldIndex(
+        MakeFieldIndex("coll", "b", model::Segment::kDescending));
+    index_manager_->UpdateIndexEntries(
+        DocumentMap({doc1, doc2, doc3, doc4, doc5, doc6}));
+    index_manager_->UpdateCollectionGroup(
+        "coll", model::IndexOffset::FromDocument(doc6));
+
+    // a IN [1,2,3] && a IN [0,1,4] should result in "a==1".
+    auto query1 = testutil::Query("coll").AddingFilter(
+        AndFilters({Filter("a", "in", Array(1, 2, 3)),
+                    Filter("a", "in", Array(0, 1, 4))}));
+    DocumentSet result1 = ExpectOptimizedCollectionScan(
+        [&] { return RunQuery(query1, SnapshotVersion::None()); });
+    EXPECT_EQ(result1, DocSet(query1.Comparator(), {doc1, doc4, doc5}));
+
+    // a IN [2,3] && a IN [0,1,4] is never true and so the result should be an
+    // empty set.
+    auto query2 = testutil::Query("coll").AddingFilter(AndFilters(
+        {Filter("a", "in", Array(2, 3)), Filter("a", "in", Array(0, 1, 4))}));
+    DocumentSet result2 = ExpectOptimizedCollectionScan(
+        [&] { return RunQuery(query2, SnapshotVersion::None()); });
+    EXPECT_EQ(result2, DocSet(query2.Comparator(), {}));
+
+    // a IN [0,3] || a IN [0,2] should union them (similar to: a IN [0,2,3]).
+    auto query3 = testutil::Query("coll").AddingFilter(OrFilters(
+        {Filter("a", "in", Array(0, 3)), Filter("a", "in", Array(0, 2))}));
+    DocumentSet result3 = ExpectOptimizedCollectionScan(
+        [&] { return RunQuery(query3, SnapshotVersion::None()); });
+    EXPECT_EQ(result3, DocSet(query3.Comparator(), {doc3, doc6}));
+
+    // Nested composite filter: (a IN [0,1,2,3] && (a IN [0,2] || (b>1 && a IN
+    // [1,3]))
+    auto query4 = testutil::Query("coll").AddingFilter(AndFilters(
+        {Filter("a", "in", Array(0, 1, 2, 3)),
+         OrFilters({Filter("a", "in", Array(0, 2)),
+                    AndFilters({Filter("b", ">=", 1),
+                                Filter("a", "in", Array(1, 3))})})}));
+    DocumentSet result4 = ExpectOptimizedCollectionScan(
+        [&] { return RunQuery(query4, SnapshotVersion::None()); });
+    EXPECT_EQ(result4, DocSet(query4.Comparator(), {doc3, doc4}));
+  });
+}
+
+TEST_F(LevelDbQueryEngineTest, QueryWithMultipleInsOnDifferentFields) {
+  persistence_->Run("QueryWithMultipleInsOnDifferentFields", [&] {
+    mutation_queue_->Start();
+    index_manager_->Start();
+
+    auto doc1 = Doc("coll/1", 1, Map("a", 1, "b", 0));
+    auto doc2 = Doc("coll/2", 1, Map("b", 1));
+    auto doc3 = Doc("coll/3", 1, Map("a", 3, "b", 2));
+    auto doc4 = Doc("coll/4", 1, Map("a", 1, "b", 3));
+    auto doc5 = Doc("coll/5", 1, Map("a", 1));
+    auto doc6 = Doc("coll/6", 1, Map("a", 2));
+    AddDocuments({doc1, doc2, doc3, doc4, doc5, doc6});
+    index_manager_->AddFieldIndex(
+        MakeFieldIndex("coll", "a", model::Segment::kAscending));
+    index_manager_->AddFieldIndex(
+        MakeFieldIndex("coll", "a", model::Segment::kDescending));
+    index_manager_->AddFieldIndex(
+        MakeFieldIndex("coll", "b", model::Segment::kAscending));
+    index_manager_->AddFieldIndex(
+        MakeFieldIndex("coll", "b", model::Segment::kDescending));
+    index_manager_->UpdateIndexEntries(
+        DocumentMap({doc1, doc2, doc3, doc4, doc5, doc6}));
+    index_manager_->UpdateCollectionGroup(
+        "coll", model::IndexOffset::FromDocument(doc6));
+
+    auto query1 = testutil::Query("coll").AddingFilter(OrFilters(
+        {Filter("a", "in", Array(2, 3)), Filter("b", "in", Array(0, 2))}));
+    DocumentSet result1 = ExpectOptimizedCollectionScan(
+        [&] { return RunQuery(query1, SnapshotVersion::None()); });
+    EXPECT_EQ(result1, DocSet(query1.Comparator(), {doc1, doc3, doc6}));
+
+    auto query2 = testutil::Query("coll").AddingFilter(AndFilters(
+        {Filter("a", "in", Array(2, 3)), Filter("b", "in", Array(0, 2))}));
+    DocumentSet result2 = ExpectOptimizedCollectionScan(
+        [&] { return RunQuery(query2, SnapshotVersion::None()); });
+    EXPECT_EQ(result2, DocSet(query2.Comparator(), {doc3}));
+
+    // Nested composite filter: (b in [0,3] && (b IN [1] || (b in [2,3] && a IN
+    // [1,3]))
+    auto query3 = testutil::Query("coll").AddingFilter(AndFilters(
+        {Filter("b", "in", Array(0, 3)),
+         OrFilters({Filter("b", "in", Array(1)),
+                    AndFilters({Filter("b", "in", Array(2, 3)),
+                                Filter("a", "in", Array(1, 3))})})}));
+    DocumentSet result3 = ExpectOptimizedCollectionScan(
+        [&] { return RunQuery(query3, SnapshotVersion::None()); });
+    EXPECT_EQ(result3, DocSet(query3.Comparator(), {doc4}));
+  });
+}
+
+TEST_F(LevelDbQueryEngineTest, QueryInWithArrayContainsAny) {
+  persistence_->Run("QueryInWithArrayContainsAny", [&] {
+    mutation_queue_->Start();
+    index_manager_->Start();
+
+    auto doc1 = Doc("coll/1", 1, Map("a", 1, "b", Array(0)));
+    auto doc2 = Doc("coll/2", 1, Map("b", Array(1)));
+    auto doc3 = Doc("coll/3", 1, Map("a", 3, "b", Array(2, 7), "c", 10));
+    auto doc4 = Doc("coll/4", 1, Map("a", 1, "b", Array(3, 7)));
+    auto doc5 = Doc("coll/5", 1, Map("a", 1));
+    auto doc6 = Doc("coll/6", 1, Map("a", 2, "c", 20));
+    AddDocuments({doc1, doc2, doc3, doc4, doc5, doc6});
+    index_manager_->AddFieldIndex(
+        MakeFieldIndex("coll", "a", model::Segment::kAscending));
+    index_manager_->AddFieldIndex(
+        MakeFieldIndex("coll", "a", model::Segment::kDescending));
+    index_manager_->AddFieldIndex(
+        MakeFieldIndex("coll", "b", model::Segment::kContains));
+    index_manager_->UpdateIndexEntries(
+        DocumentMap({doc1, doc2, doc3, doc4, doc5, doc6}));
+    index_manager_->UpdateCollectionGroup(
+        "coll", model::IndexOffset::FromDocument(doc6));
+
+    auto query1 = testutil::Query("coll").AddingFilter(
+        OrFilters({Filter("a", "in", Array(2, 3)),
+                   Filter("b", "array-contains-any", Array(0, 7))}));
+    DocumentSet result1 = ExpectOptimizedCollectionScan(
+        [&] { return RunQuery(query1, SnapshotVersion::None()); });
+    EXPECT_EQ(result1, DocSet(query1.Comparator(), {doc1, doc3, doc4, doc6}));
+
+    auto query2 = testutil::Query("coll").AddingFilter(
+        AndFilters({Filter("a", "in", Array(2, 3)),
+                    Filter("b", "array-contains-any", Array(0, 7))}));
+    DocumentSet result2 = ExpectOptimizedCollectionScan(
+        [&] { return RunQuery(query2, SnapshotVersion::None()); });
+    EXPECT_EQ(result2, DocSet(query2.Comparator(), {doc3}));
+
+    auto query3 = testutil::Query("coll").AddingFilter(OrFilters(
+        {AndFilters({Filter("a", "in", Array(2, 3)), Filter("c", "==", 10)}),
+         Filter("b", "array-contains-any", Array(0, 7))}));
+    DocumentSet result3 = ExpectOptimizedCollectionScan(
+        [&] { return RunQuery(query3, SnapshotVersion::None()); });
+    EXPECT_EQ(result3, DocSet(query3.Comparator(), {doc1, doc3, doc4}));
+
+    auto query4 = testutil::Query("coll").AddingFilter(
+        AndFilters({Filter("a", "in", Array(2, 3)),
+                    OrFilters({Filter("b", "array-contains-any", Array(0, 7)),
+                               Filter("c", "==", 20)})}));
+    DocumentSet result4 = ExpectOptimizedCollectionScan(
+        [&] { return RunQuery(query4, SnapshotVersion::None()); });
+    EXPECT_EQ(result4, DocSet(query4.Comparator(), {doc3, doc6}));
+  });
+}
+
+TEST_F(LevelDbQueryEngineTest, QueryInWithArrayContains) {
+  persistence_->Run("QueryInWithArrayContains", [&] {
+    mutation_queue_->Start();
+    index_manager_->Start();
+
+    auto doc1 = Doc("coll/1", 1, Map("a", 1, "b", Array(0)));
+    auto doc2 = Doc("coll/2", 1, Map("b", Array(1)));
+    auto doc3 = Doc("coll/3", 1, Map("a", 3, "b", Array(2, 7), "c", 10));
+    auto doc4 = Doc("coll/4", 1, Map("a", 1, "b", Array(3, 7)));
+    auto doc5 = Doc("coll/5", 1, Map("a", 1));
+    auto doc6 = Doc("coll/6", 1, Map("a", 2, "c", 20));
+    AddDocuments({doc1, doc2, doc3, doc4, doc5, doc6});
+    index_manager_->AddFieldIndex(
+        MakeFieldIndex("coll", "a", model::Segment::kAscending));
+    index_manager_->AddFieldIndex(
+        MakeFieldIndex("coll", "a", model::Segment::kDescending));
+    index_manager_->AddFieldIndex(
+        MakeFieldIndex("coll", "b", model::Segment::kContains));
+    index_manager_->UpdateIndexEntries(
+        DocumentMap({doc1, doc2, doc3, doc4, doc5, doc6}));
+    index_manager_->UpdateCollectionGroup(
+        "coll", model::IndexOffset::FromDocument(doc6));
+
+    auto query1 = testutil::Query("coll").AddingFilter(OrFilters(
+        {Filter("a", "in", Array(2, 3)), Filter("b", "array-contains", 3)}));
+    DocumentSet result1 = ExpectOptimizedCollectionScan(
+        [&] { return RunQuery(query1, SnapshotVersion::None()); });
+    EXPECT_EQ(result1, DocSet(query1.Comparator(), {doc3, doc4, doc6}));
+
+    auto query2 = testutil::Query("coll").AddingFilter(AndFilters(
+        {Filter("a", "in", Array(2, 3)), Filter("b", "array-contains", 7)}));
+    DocumentSet result2 = ExpectOptimizedCollectionScan(
+        [&] { return RunQuery(query2, SnapshotVersion::None()); });
+    EXPECT_EQ(result2, DocSet(query2.Comparator(), {doc3}));
+
+    auto query3 = testutil::Query("coll").AddingFilter(
+        OrFilters({Filter("a", "in", Array(2, 3)),
+                   AndFilters({Filter("b", "array-contains", 3),
+                               Filter("a", "==", 1)})}));
+    DocumentSet result3 = ExpectOptimizedCollectionScan(
+        [&] { return RunQuery(query3, SnapshotVersion::None()); });
+    EXPECT_EQ(result3, DocSet(query3.Comparator(), {doc3, doc4, doc6}));
+
+    auto query4 = testutil::Query("coll").AddingFilter(AndFilters(
+        {Filter("a", "in", Array(2, 3)),
+         OrFilters({Filter("b", "array-contains", 7), Filter("a", "==", 1)})}));
+    DocumentSet result4 = ExpectOptimizedCollectionScan(
+        [&] { return RunQuery(query4, SnapshotVersion::None()); });
+    EXPECT_EQ(result4, DocSet(query4.Comparator(), {doc3}));
+  });
+}
+
+TEST_F(LevelDbQueryEngineTest, OrderByEquality) {
+  persistence_->Run("OrderByEquality", [&] {
+    mutation_queue_->Start();
+    index_manager_->Start();
+
+    auto doc1 = Doc("coll/1", 1, Map("a", 1, "b", Array(0)));
+    auto doc2 = Doc("coll/2", 1, Map("b", Array(1)));
+    auto doc3 = Doc("coll/3", 1, Map("a", 3, "b", Array(2, 7), "c", 10));
+    auto doc4 = Doc("coll/4", 1, Map("a", 1, "b", Array(3, 7)));
+    auto doc5 = Doc("coll/5", 1, Map("a", 1));
+    auto doc6 = Doc("coll/6", 1, Map("a", 2, "c", 20));
+    AddDocuments({doc1, doc2, doc3, doc4, doc5, doc6});
+    index_manager_->AddFieldIndex(
+        MakeFieldIndex("coll", "a", model::Segment::kAscending));
+    index_manager_->AddFieldIndex(
+        MakeFieldIndex("coll", "a", model::Segment::kDescending));
+    index_manager_->AddFieldIndex(
+        MakeFieldIndex("coll", "b", model::Segment::kContains));
+    index_manager_->UpdateIndexEntries(
+        DocumentMap({doc1, doc2, doc3, doc4, doc5, doc6}));
+    index_manager_->UpdateCollectionGroup(
+        "coll", model::IndexOffset::FromDocument(doc6));
+
+    auto query1 = testutil::Query("coll")
+                      .AddingFilter(Filter("a", "==", 1))
+                      .AddingOrderBy(OrderBy("a"));
+    DocumentSet result1 = ExpectOptimizedCollectionScan(
+        [&] { return RunQuery(query1, SnapshotVersion::None()); });
+    EXPECT_EQ(result1, DocSet(query1.Comparator(), {doc1, doc4, doc5}));
+
+    auto query2 = testutil::Query("coll")
+                      .AddingFilter(Filter("a", "in", Array(2, 3)))
+                      .AddingOrderBy(OrderBy("a"));
+    DocumentSet result2 = ExpectOptimizedCollectionScan(
+        [&] { return RunQuery(query2, SnapshotVersion::None()); });
+    EXPECT_EQ(result2, DocSet(query2.Comparator(), {doc6, doc3}));
+  });
+}
+
 }  // namespace local
 }  // namespace firestore
 }  // namespace firebase

+ 183 - 0
Firestore/core/test/unit/local/query_engine_test.cc

@@ -746,6 +746,189 @@ TEST_P(QueryEngineTest, OrQueryWithArrayMembership) {
   });
 }
 
+TEST_P(QueryEngineTest, QueryWithMultipleInsOnTheSameField) {
+  persistence_->Run("QueryWithMultipleInsOnTheSameField", [&] {
+    mutation_queue_->Start();
+    index_manager_->Start();
+
+    MutableDocument doc1 = Doc("coll/1", 1, Map("a", 1, "b", 0));
+    MutableDocument doc2 = Doc("coll/2", 1, Map("b", 1));
+    MutableDocument doc3 = Doc("coll/3", 1, Map("a", 3, "b", 2));
+    MutableDocument doc4 = Doc("coll/4", 1, Map("a", 1, "b", 3));
+    MutableDocument doc5 = Doc("coll/5", 1, Map("a", 1));
+    MutableDocument doc6 = Doc("coll/6", 1, Map("a", 2));
+    AddDocuments({doc1, doc2, doc3, doc4, doc5, doc6});
+
+    // a IN [1,2,3] && a IN [0,1,4] should result in "a==1".
+    auto query1 = testutil::Query("coll").AddingFilter(
+        AndFilters({Filter("a", "in", Array(1, 2, 3)),
+                    Filter("a", "in", Array(0, 1, 4))}));
+    DocumentSet result1 = ExpectFullCollectionScan<DocumentSet>(
+        [&] { return RunQuery(query1, kMissingLastLimboFreeSnapshot); });
+    EXPECT_EQ(result1, DocSet(query1.Comparator(), {doc1, doc4, doc5}));
+
+    // a IN [2,3] && a IN [0,1,4] is never true and so the result should be an
+    // empty set.
+    auto query2 = testutil::Query("coll").AddingFilter(AndFilters(
+        {Filter("a", "in", Array(2, 3)), Filter("a", "in", Array(0, 1, 4))}));
+    DocumentSet result2 = ExpectFullCollectionScan<DocumentSet>(
+        [&] { return RunQuery(query2, kMissingLastLimboFreeSnapshot); });
+    EXPECT_EQ(result2, DocSet(query2.Comparator(), {}));
+
+    // a IN [0,3] || a IN [0,2] should union them (similar to: a IN [0,2,3]).
+    auto query3 = testutil::Query("coll").AddingFilter(OrFilters(
+        {Filter("a", "in", Array(0, 3)), Filter("a", "in", Array(0, 2))}));
+    DocumentSet result3 = ExpectFullCollectionScan<DocumentSet>(
+        [&] { return RunQuery(query3, kMissingLastLimboFreeSnapshot); });
+    EXPECT_EQ(result3, DocSet(query3.Comparator(), {doc3, doc6}));
+  });
+}
+
+TEST_P(QueryEngineTest, QueryWithMultipleInsOnDifferentFields) {
+  persistence_->Run("QueryWithMultipleInsOnDifferentFields", [&] {
+    mutation_queue_->Start();
+    index_manager_->Start();
+
+    MutableDocument doc1 = Doc("coll/1", 1, Map("a", 1, "b", 0));
+    MutableDocument doc2 = Doc("coll/2", 1, Map("b", 1));
+    MutableDocument doc3 = Doc("coll/3", 1, Map("a", 3, "b", 2));
+    MutableDocument doc4 = Doc("coll/4", 1, Map("a", 1, "b", 3));
+    MutableDocument doc5 = Doc("coll/5", 1, Map("a", 1));
+    MutableDocument doc6 = Doc("coll/6", 1, Map("a", 2));
+    AddDocuments({doc1, doc2, doc3, doc4, doc5, doc6});
+
+    auto query1 = testutil::Query("coll").AddingFilter(OrFilters(
+        {Filter("a", "in", Array(2, 3)), Filter("b", "in", Array(0, 2))}));
+    DocumentSet result1 = ExpectFullCollectionScan<DocumentSet>(
+        [&] { return RunQuery(query1, kMissingLastLimboFreeSnapshot); });
+    EXPECT_EQ(result1, DocSet(query1.Comparator(), {doc1, doc3, doc6}));
+
+    auto query2 = testutil::Query("coll").AddingFilter(AndFilters(
+        {Filter("a", "in", Array(2, 3)), Filter("b", "in", Array(0, 2))}));
+    DocumentSet result2 = ExpectFullCollectionScan<DocumentSet>(
+        [&] { return RunQuery(query2, kMissingLastLimboFreeSnapshot); });
+    EXPECT_EQ(result2, DocSet(query2.Comparator(), {doc3}));
+  });
+}
+
+TEST_P(QueryEngineTest, QueryInWithArrayContainsAny) {
+  persistence_->Run("QueryInWithArrayContainsAny", [&] {
+    mutation_queue_->Start();
+    index_manager_->Start();
+
+    MutableDocument doc1 = Doc("coll/1", 1, Map("a", 1, "b", Array(0)));
+    MutableDocument doc2 = Doc("coll/2", 1, Map("b", Array(1)));
+    MutableDocument doc3 =
+        Doc("coll/3", 1, Map("a", 3, "b", Array(2, 7), "c", 10));
+    MutableDocument doc4 = Doc("coll/4", 1, Map("a", 1, "b", Array(3, 7)));
+    MutableDocument doc5 = Doc("coll/5", 1, Map("a", 1));
+    MutableDocument doc6 = Doc("coll/6", 1, Map("a", 2, "c", 20));
+    AddDocuments({doc1, doc2, doc3, doc4, doc5, doc6});
+
+    auto query1 = testutil::Query("coll").AddingFilter(
+        OrFilters({Filter("a", "in", Array(2, 3)),
+                   Filter("b", "array-contains-any", Array(0, 7))}));
+    DocumentSet result1 = ExpectFullCollectionScan<DocumentSet>(
+        [&] { return RunQuery(query1, kMissingLastLimboFreeSnapshot); });
+    EXPECT_EQ(result1, DocSet(query1.Comparator(), {doc1, doc3, doc4, doc6}));
+
+    auto query2 = testutil::Query("coll").AddingFilter(
+        AndFilters({Filter("a", "in", Array(2, 3)),
+                    Filter("b", "array-contains-any", Array(0, 7))}));
+    DocumentSet result2 = ExpectFullCollectionScan<DocumentSet>(
+        [&] { return RunQuery(query2, kMissingLastLimboFreeSnapshot); });
+    EXPECT_EQ(result2, DocSet(query2.Comparator(), {doc3}));
+
+    auto query3 = testutil::Query("coll").AddingFilter(OrFilters(
+        {AndFilters({Filter("a", "in", Array(2, 3)), Filter("c", "==", 10)}),
+         Filter("b", "array-contains-any", Array(0, 7))}));
+    DocumentSet result3 = ExpectFullCollectionScan<DocumentSet>(
+        [&] { return RunQuery(query3, kMissingLastLimboFreeSnapshot); });
+    EXPECT_EQ(result3, DocSet(query3.Comparator(), {doc1, doc3, doc4}));
+
+    auto query4 = testutil::Query("coll").AddingFilter(
+        AndFilters({Filter("a", "in", Array(2, 3)),
+                    OrFilters({Filter("b", "array-contains-any", Array(0, 7)),
+                               Filter("c", "==", 20)})}));
+    DocumentSet result4 = ExpectFullCollectionScan<DocumentSet>(
+        [&] { return RunQuery(query4, kMissingLastLimboFreeSnapshot); });
+    EXPECT_EQ(result4, DocSet(query4.Comparator(), {doc3, doc6}));
+  });
+}
+
+TEST_P(QueryEngineTest, QueryInWithArrayContains) {
+  persistence_->Run("QueryInWithArrayContains", [&] {
+    mutation_queue_->Start();
+    index_manager_->Start();
+
+    MutableDocument doc1 = Doc("coll/1", 1, Map("a", 1, "b", Array(0)));
+    MutableDocument doc2 = Doc("coll/2", 1, Map("b", Array(1)));
+    MutableDocument doc3 =
+        Doc("coll/3", 1, Map("a", 3, "b", Array(2, 7), "c", 10));
+    MutableDocument doc4 = Doc("coll/4", 1, Map("a", 1, "b", Array(3, 7)));
+    MutableDocument doc5 = Doc("coll/5", 1, Map("a", 1));
+    MutableDocument doc6 = Doc("coll/6", 1, Map("a", 2, "c", 20));
+    AddDocuments({doc1, doc2, doc3, doc4, doc5, doc6});
+
+    auto query1 = testutil::Query("coll").AddingFilter(OrFilters(
+        {Filter("a", "in", Array(2, 3)), Filter("b", "array-contains", 3)}));
+    DocumentSet result1 = ExpectFullCollectionScan<DocumentSet>(
+        [&] { return RunQuery(query1, kMissingLastLimboFreeSnapshot); });
+    EXPECT_EQ(result1, DocSet(query1.Comparator(), {doc3, doc4, doc6}));
+
+    auto query2 = testutil::Query("coll").AddingFilter(AndFilters(
+        {Filter("a", "in", Array(2, 3)), Filter("b", "array-contains", 7)}));
+    DocumentSet result2 = ExpectFullCollectionScan<DocumentSet>(
+        [&] { return RunQuery(query2, kMissingLastLimboFreeSnapshot); });
+    EXPECT_EQ(result2, DocSet(query2.Comparator(), {doc3}));
+
+    auto query3 = testutil::Query("coll").AddingFilter(
+        OrFilters({Filter("a", "in", Array(2, 3)),
+                   AndFilters({Filter("b", "array-contains", 3),
+                               Filter("a", "==", 1)})}));
+    DocumentSet result3 = ExpectFullCollectionScan<DocumentSet>(
+        [&] { return RunQuery(query3, kMissingLastLimboFreeSnapshot); });
+    EXPECT_EQ(result3, DocSet(query3.Comparator(), {doc3, doc4, doc6}));
+
+    auto query4 = testutil::Query("coll").AddingFilter(AndFilters(
+        {Filter("a", "in", Array(2, 3)),
+         OrFilters({Filter("b", "array-contains", 7), Filter("a", "==", 1)})}));
+    DocumentSet result4 = ExpectFullCollectionScan<DocumentSet>(
+        [&] { return RunQuery(query4, kMissingLastLimboFreeSnapshot); });
+    EXPECT_EQ(result4, DocSet(query4.Comparator(), {doc3}));
+  });
+}
+
+TEST_P(QueryEngineTest, OrderByEquality) {
+  persistence_->Run("OrderByEquality", [&] {
+    mutation_queue_->Start();
+    index_manager_->Start();
+
+    MutableDocument doc1 = Doc("coll/1", 1, Map("a", 1, "b", Array(0)));
+    MutableDocument doc2 = Doc("coll/2", 1, Map("b", Array(1)));
+    MutableDocument doc3 =
+        Doc("coll/3", 1, Map("a", 3, "b", Array(2, 7), "c", 10));
+    MutableDocument doc4 = Doc("coll/4", 1, Map("a", 1, "b", Array(3, 7)));
+    MutableDocument doc5 = Doc("coll/5", 1, Map("a", 1));
+    MutableDocument doc6 = Doc("coll/6", 1, Map("a", 2, "c", 20));
+    AddDocuments({doc1, doc2, doc3, doc4, doc5, doc6});
+
+    auto query1 = testutil::Query("coll")
+                      .AddingFilter(Filter("a", "==", 1))
+                      .AddingOrderBy(OrderBy("a"));
+    DocumentSet result1 = ExpectFullCollectionScan<DocumentSet>(
+        [&] { return RunQuery(query1, kMissingLastLimboFreeSnapshot); });
+    EXPECT_EQ(result1, DocSet(query1.Comparator(), {doc1, doc4, doc5}));
+
+    auto query2 = testutil::Query("coll")
+                      .AddingFilter(Filter("a", "in", Array(2, 3)))
+                      .AddingOrderBy(OrderBy("a"));
+    DocumentSet result2 = ExpectFullCollectionScan<DocumentSet>(
+        [&] { return RunQuery(query2, kMissingLastLimboFreeSnapshot); });
+    EXPECT_EQ(result2, DocSet(query2.Comparator(), {doc6, doc3}));
+  });
+}
+
 }  // namespace local
 }  // namespace firestore
 }  // namespace firebase

+ 72 - 0
Firestore/core/test/unit/util/logic_utils_test.cc

@@ -29,6 +29,7 @@ namespace util {
 using core::CompositeFilter;
 using core::FieldFilter;
 using testutil::AndFilters;
+using testutil::Array;
 using testutil::OrFilters;
 
 namespace {
@@ -277,6 +278,77 @@ TEST_F(LogicUtilsTest, ComputeDnf8) {
             std::vector<core::Filter>{expected_dnf_terms});
 }
 
+TEST_F(LogicUtilsTest, InExpansionForFieldFilters) {
+  auto input1 = testutil::Filter("a", "in", Array(1, 2, 3));
+  auto input2 = testutil::Filter("a", "<", 1);
+  auto input3 = testutil::Filter("a", "<=", 1);
+  auto input4 = testutil::Filter("a", "==", 1);
+  auto input5 = testutil::Filter("a", "!=", 1);
+  auto input6 = testutil::Filter("a", ">", 1);
+  auto input7 = testutil::Filter("a", ">=", 1);
+  auto input8 = testutil::Filter("a", "array-contains", 1);
+  auto input9 = testutil::Filter("a", "array-contains-any", Array(1, 2));
+  auto input10 = testutil::Filter("a", "not-in", Array(1, 2));
+
+  EXPECT_EQ(
+      ComputeInExpansion(input1),
+      OrFilters({testutil::Filter("a", "==", 1), testutil::Filter("a", "==", 2),
+                 testutil::Filter("a", "==", 3)}));
+
+  // Other operators should remain the same
+  EXPECT_EQ(ComputeInExpansion(input2), input2);
+  EXPECT_EQ(ComputeInExpansion(input3), input3);
+  EXPECT_EQ(ComputeInExpansion(input4), input4);
+  EXPECT_EQ(ComputeInExpansion(input5), input5);
+  EXPECT_EQ(ComputeInExpansion(input6), input6);
+  EXPECT_EQ(ComputeInExpansion(input7), input7);
+  EXPECT_EQ(ComputeInExpansion(input8), input8);
+  EXPECT_EQ(ComputeInExpansion(input9), input9);
+  EXPECT_EQ(ComputeInExpansion(input10), input10);
+}
+
+TEST_F(LogicUtilsTest, InExpansionForCompositeFilters) {
+  auto cf1 = AndFilters({testutil::Filter("a", "==", 1),
+                         testutil::Filter("b", "in", Array(2, 3, 4))});
+  EXPECT_EQ(ComputeInExpansion(cf1),
+            AndFilters({testutil::Filter("a", "==", 1),
+                        OrFilters({testutil::Filter("b", "==", 2),
+                                   testutil::Filter("b", "==", 3),
+                                   testutil::Filter("b", "==", 4)})}));
+
+  auto cf2 = OrFilters({testutil::Filter("a", "==", 1),
+                        testutil::Filter("b", "in", Array(2, 3, 4))});
+  EXPECT_EQ(ComputeInExpansion(cf2),
+            OrFilters({testutil::Filter("a", "==", 1),
+                       OrFilters({testutil::Filter("b", "==", 2),
+                                  testutil::Filter("b", "==", 3),
+                                  testutil::Filter("b", "==", 4)})}));
+
+  auto cf3 =
+      AndFilters({testutil::Filter("a", "==", 1),
+                  OrFilters({testutil::Filter("b", "==", 2),
+                             testutil::Filter("c", "in", Array(2, 3, 4))})});
+  EXPECT_EQ(
+      ComputeInExpansion(cf3),
+      AndFilters({testutil::Filter("a", "==", 1),
+                  OrFilters({testutil::Filter("b", "==", 2),
+                             OrFilters({testutil::Filter("c", "==", 2),
+                                        testutil::Filter("c", "==", 3),
+                                        testutil::Filter("c", "==", 4)})})}));
+
+  CompositeFilter cf4 =
+      OrFilters({testutil::Filter("a", "==", 1),
+                 AndFilters({testutil::Filter("b", "==", 2),
+                             testutil::Filter("c", "in", Array(2, 3, 4))})});
+  EXPECT_EQ(
+      ComputeInExpansion(cf4),
+      OrFilters({testutil::Filter("a", "==", 1),
+                 AndFilters({testutil::Filter("b", "==", 2),
+                             OrFilters({testutil::Filter("c", "==", 2),
+                                        testutil::Filter("c", "==", 3),
+                                        testutil::Filter("c", "==", 4)})})}));
+}
+
 }  // namespace util
 }  // namespace firestore
 }  // namespace firebase