Skip to content

Commit 093155c

Browse files
committed
Serving OR Queries from the client-side index.
1 parent 2bac92d commit 093155c

File tree

3 files changed

+161
-8
lines changed

3 files changed

+161
-8
lines changed

firebase-firestore/src/main/java/com/google/firebase/firestore/local/SQLiteIndexManager.java

Lines changed: 31 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -314,7 +314,9 @@ public IndexOffset getMinOffset(String collectionGroup) {
314314
@Override
315315
public IndexType getIndexType(Target target) {
316316
IndexType result = IndexType.FULL;
317-
for (Target subTarget : getSubTargets(target)) {
317+
List<Target> subTargets = getSubTargets(target);
318+
319+
for (Target subTarget : subTargets) {
318320
FieldIndex index = getFieldIndex(subTarget);
319321
if (index == null) {
320322
result = IndexType.NONE;
@@ -325,6 +327,16 @@ public IndexType getIndexType(Target target) {
325327
result = IndexType.PARTIAL;
326328
}
327329
}
330+
331+
// OR queries have more than one sub-target (one sub-target per DNF term). We currently consider
332+
// all OR queries to have partial indexes, and hence do sorting and limit in post-processing.
333+
// TODO(orquery): If we have a FULL index *and* we have the index that can be used for sorting
334+
// all DNF branches on the same value, we can improve performance by performing a JOIN in SQL.
335+
// See b/235224019 for more information.
336+
if (subTargets.size() > 1 && result == IndexType.FULL) {
337+
return IndexType.PARTIAL;
338+
}
339+
328340
return result;
329341
}
330342

@@ -509,9 +521,23 @@ public List<DocumentKey> getDocumentsMatchingTarget(Target target) {
509521
bindings.addAll(Arrays.asList(subQueryAndBindings).subList(1, subQueryAndBindings.length));
510522
}
511523

512-
String queryString =
513-
"SELECT DISTINCT document_key FROM (" + TextUtils.join(" UNION ", subQueries) + ")";
514-
524+
// We are constructing:
525+
// SELECT DISTINCT document_key FROM (
526+
// (SELECT ...) UNION (SELECT ...) UNION (SELECT ...)
527+
// ORDER BY ...
528+
// )
529+
// LIMIT ...
530+
//
531+
// Note: SQLite does not allow performing ORDER BY on each union clause. The ORDER BY must come
532+
// after the last union clause. Also note that LIMIT must be applied *after* the DISTINCT
533+
// operator has been performed. When dealing with multiple sub-targets, it's possible that the
534+
// same document_key appears multiple times.
535+
String unionSubTargets =
536+
TextUtils.join(" UNION ", subQueries)
537+
+ "ORDER BY directional_value, document_key "
538+
+ (target.getKeyOrder().equals(Direction.ASCENDING) ? "asc " : "desc ");
539+
540+
String queryString = "SELECT DISTINCT document_key FROM (" + unionSubTargets + ")";
515541
if (target.hasLimit()) {
516542
queryString = queryString + " LIMIT " + target.getLimit();
517543
}
@@ -557,11 +583,8 @@ private Object[] generateQueryAndBindings(
557583
statement.append("AND directional_value ").append(lowerBoundOp).append(" ? ");
558584
statement.append("AND directional_value ").append(upperBoundOp).append(" ? ");
559585

560-
// Create the UNION statement by repeating the above generated statement. We can then add
561-
// ordering and a limit clause.
586+
// Create the UNION statement by repeating the above generated statement.
562587
StringBuilder sql = repeatSequence(statement, statementCount, " UNION ");
563-
sql.append("ORDER BY directional_value, document_key ");
564-
sql.append(target.getKeyOrder().equals(Direction.ASCENDING) ? "asc " : "desc ");
565588

566589
if (notIn != null) {
567590
// Wrap the statement in a NOT-IN call.

firebase-firestore/src/test/java/com/google/firebase/firestore/local/QueryEngineTestCase.java

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -482,5 +482,39 @@ public void canPerformOrQueriesUsingFullCollectionScan() throws Exception {
482482
DocumentSet result5 =
483483
expectFullCollectionScan(() -> runQuery(query5, MISSING_LAST_LIMBO_FREE_SNAPSHOT));
484484
assertEquals(docSet(query5.comparator(), doc3), result5);
485+
486+
// Test with limits (implicit order by ASC): (a==1) || (b > 0) LIMIT 2
487+
Query query6 =
488+
query("coll").filter(orFilters(filter("a", "==", 1), filter("b", ">", 0))).limitToFirst(2);
489+
DocumentSet result6 =
490+
expectFullCollectionScan(() -> runQuery(query6, MISSING_LAST_LIMBO_FREE_SNAPSHOT));
491+
assertEquals(docSet(query6.comparator(), doc1, doc2), result6);
492+
493+
// Test with limits (implicit order by DESC): (a==1) || (b > 0) LIMIT_TO_LAST 2
494+
Query query7 =
495+
query("coll").filter(orFilters(filter("a", "==", 1), filter("b", ">", 0))).limitToLast(2);
496+
DocumentSet result7 =
497+
expectFullCollectionScan(() -> runQuery(query7, MISSING_LAST_LIMBO_FREE_SNAPSHOT));
498+
assertEquals(docSet(query7.comparator(), doc3, doc4), result7);
499+
500+
// Test with limits (explicit order by ASC): (a==2) || (b == 1) ORDER BY a LIMIT 1
501+
Query query8 =
502+
query("coll")
503+
.filter(orFilters(filter("a", "==", 2), filter("b", "==", 1)))
504+
.limitToFirst(1)
505+
.orderBy(orderBy("a", "asc"));
506+
DocumentSet result8 =
507+
expectFullCollectionScan(() -> runQuery(query8, MISSING_LAST_LIMBO_FREE_SNAPSHOT));
508+
assertEquals(docSet(query8.comparator(), doc5), result8);
509+
510+
// Test with limits (explicit order by DESC): (a==2) || (b == 1) ORDER BY a LIMIT_TO_LAST 1
511+
Query query9 =
512+
query("coll")
513+
.filter(orFilters(filter("a", "==", 2), filter("b", "==", 1)))
514+
.limitToLast(1)
515+
.orderBy(orderBy("a", "asc"));
516+
DocumentSet result9 =
517+
expectFullCollectionScan(() -> runQuery(query9, MISSING_LAST_LIMBO_FREE_SNAPSHOT));
518+
assertEquals(docSet(query9.comparator(), doc2), result9);
485519
}
486520
}

firebase-firestore/src/test/java/com/google/firebase/firestore/local/SQLiteQueryEngineTest.java

Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,12 +17,14 @@
1717
import static com.google.firebase.firestore.local.IndexManager.*;
1818
import static com.google.firebase.firestore.model.FieldIndex.*;
1919
import static com.google.firebase.firestore.model.FieldIndex.Segment.*;
20+
import static com.google.firebase.firestore.testutil.TestUtil.andFilters;
2021
import static com.google.firebase.firestore.testutil.TestUtil.doc;
2122
import static com.google.firebase.firestore.testutil.TestUtil.docMap;
2223
import static com.google.firebase.firestore.testutil.TestUtil.docSet;
2324
import static com.google.firebase.firestore.testutil.TestUtil.fieldIndex;
2425
import static com.google.firebase.firestore.testutil.TestUtil.filter;
2526
import static com.google.firebase.firestore.testutil.TestUtil.map;
27+
import static com.google.firebase.firestore.testutil.TestUtil.orFilters;
2628
import static com.google.firebase.firestore.testutil.TestUtil.orderBy;
2729
import static com.google.firebase.firestore.testutil.TestUtil.patchMutation;
2830
import static com.google.firebase.firestore.testutil.TestUtil.query;
@@ -108,4 +110,98 @@ public void testRefillsIndexedLimitQueries() throws Exception {
108110
DocumentSet result = expectOptimizedCollectionScan(() -> runQuery(query, SnapshotVersion.NONE));
109111
assertEquals(docSet(query.comparator(), doc1, doc2, doc4), result);
110112
}
113+
114+
@Test
115+
public void canPerformOrQueriesUsingIndexes() throws Exception {
116+
MutableDocument doc1 = doc("coll/1", 1, map("a", 1, "b", 0));
117+
MutableDocument doc2 = doc("coll/2", 1, map("a", 2, "b", 1));
118+
MutableDocument doc3 = doc("coll/3", 1, map("a", 3, "b", 2));
119+
MutableDocument doc4 = doc("coll/4", 1, map("a", 1, "b", 3));
120+
MutableDocument doc5 = doc("coll/5", 1, map("a", 1, "b", 1));
121+
addDocument(doc1, doc2, doc3, doc4, doc5);
122+
indexManager.addFieldIndex(fieldIndex("coll", "a", Kind.ASCENDING));
123+
indexManager.addFieldIndex(fieldIndex("coll", "a", Kind.DESCENDING));
124+
indexManager.addFieldIndex(fieldIndex("coll", "b", Kind.ASCENDING));
125+
indexManager.addFieldIndex(fieldIndex("coll", "b", Kind.DESCENDING));
126+
indexManager.updateIndexEntries(docMap(doc1, doc2, doc3, doc4, doc5));
127+
indexManager.updateCollectionGroup("coll", IndexOffset.fromDocument(doc5));
128+
129+
// Two equalities: a==1 || b==1.
130+
Query query1 = query("coll").filter(orFilters(filter("a", "==", 1), filter("b", "==", 1)));
131+
DocumentSet result1 =
132+
expectOptimizedCollectionScan(() -> runQuery(query1, SnapshotVersion.NONE));
133+
assertEquals(docSet(query1.comparator(), doc1, doc2, doc4, doc5), result1);
134+
135+
// with one inequality: a>2 || b==1.
136+
Query query2 = query("coll").filter(orFilters(filter("a", ">", 2), filter("b", "==", 1)));
137+
DocumentSet result2 =
138+
expectOptimizedCollectionScan(() -> runQuery(query2, SnapshotVersion.NONE));
139+
assertEquals(docSet(query2.comparator(), doc2, doc3, doc5), result2);
140+
141+
// (a==1 && b==0) || (a==3 && b==2)
142+
Query query3 =
143+
query("coll")
144+
.filter(
145+
orFilters(
146+
andFilters(filter("a", "==", 1), filter("b", "==", 0)),
147+
andFilters(filter("a", "==", 3), filter("b", "==", 2))));
148+
DocumentSet result3 =
149+
expectOptimizedCollectionScan(() -> runQuery(query3, SnapshotVersion.NONE));
150+
assertEquals(docSet(query3.comparator(), doc1, doc3), result3);
151+
152+
// a==1 && (b==0 || b==3).
153+
Query query4 =
154+
query("coll")
155+
.filter(
156+
andFilters(
157+
filter("a", "==", 1), orFilters(filter("b", "==", 0), filter("b", "==", 3))));
158+
DocumentSet result4 =
159+
expectOptimizedCollectionScan(() -> runQuery(query4, SnapshotVersion.NONE));
160+
assertEquals(docSet(query4.comparator(), doc1, doc4), result4);
161+
162+
// (a==2 || b==2) && (a==3 || b==3)
163+
Query query5 =
164+
query("coll")
165+
.filter(
166+
andFilters(
167+
orFilters(filter("a", "==", 2), filter("b", "==", 2)),
168+
orFilters(filter("a", "==", 3), filter("b", "==", 3))));
169+
DocumentSet result5 =
170+
expectOptimizedCollectionScan(() -> runQuery(query5, SnapshotVersion.NONE));
171+
assertEquals(docSet(query5.comparator(), doc3), result5);
172+
173+
// Test with limits (implicit order by ASC): (a==1) || (b > 0) LIMIT 2
174+
Query query6 =
175+
query("coll").filter(orFilters(filter("a", "==", 1), filter("b", ">", 0))).limitToFirst(2);
176+
DocumentSet result6 =
177+
expectOptimizedCollectionScan(() -> runQuery(query6, SnapshotVersion.NONE));
178+
assertEquals(docSet(query6.comparator(), doc1, doc2), result6);
179+
180+
// Test with limits (implicit order by DESC): (a==1) || (b > 0) LIMIT_TO_LAST 2
181+
Query query7 =
182+
query("coll").filter(orFilters(filter("a", "==", 1), filter("b", ">", 0))).limitToLast(2);
183+
DocumentSet result7 =
184+
expectOptimizedCollectionScan(() -> runQuery(query7, SnapshotVersion.NONE));
185+
assertEquals(docSet(query7.comparator(), doc3, doc4), result7);
186+
187+
// Test with limits (explicit order by ASC): (a==2) || (b == 1) ORDER BY a LIMIT 1
188+
Query query8 =
189+
query("coll")
190+
.filter(orFilters(filter("a", "==", 2), filter("b", "==", 1)))
191+
.limitToFirst(1)
192+
.orderBy(orderBy("a", "asc"));
193+
DocumentSet result8 =
194+
expectOptimizedCollectionScan(() -> runQuery(query8, SnapshotVersion.NONE));
195+
assertEquals(docSet(query8.comparator(), doc5), result8);
196+
197+
// Test with limits (explicit order by DESC): (a==2) || (b == 1) ORDER BY a LIMIT_TO_LAST 1
198+
Query query9 =
199+
query("coll")
200+
.filter(orFilters(filter("a", "==", 2), filter("b", "==", 1)))
201+
.limitToLast(1)
202+
.orderBy(orderBy("a", "asc"));
203+
DocumentSet result9 =
204+
expectOptimizedCollectionScan(() -> runQuery(query9, SnapshotVersion.NONE));
205+
assertEquals(docSet(query9.comparator(), doc2), result9);
206+
}
111207
}

0 commit comments

Comments
 (0)