Skip to content

Commit 156b2a0

Browse files
Merge branch 'mrschmidt/indexfree-5' into mrschmidt/indexfree-4
2 parents cc295e9 + ea082f3 commit 156b2a0

File tree

3 files changed

+295
-1
lines changed

3 files changed

+295
-1
lines changed
Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
// Copyright 2019 Google LLC
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
package com.google.firebase.firestore.local;
16+
17+
import com.google.firebase.database.collection.ImmutableSortedMap;
18+
import com.google.firebase.database.collection.ImmutableSortedSet;
19+
import com.google.firebase.firestore.core.Query;
20+
import com.google.firebase.firestore.model.Document;
21+
import com.google.firebase.firestore.model.DocumentKey;
22+
import com.google.firebase.firestore.model.MaybeDocument;
23+
import com.google.firebase.firestore.model.SnapshotVersion;
24+
import java.util.Map;
25+
import javax.annotation.Nullable;
26+
27+
public class IndexFreeQueryEngine implements QueryEngine {
28+
private final LocalDocumentsView localDocumentsView;
29+
private final QueryCache queryCache;
30+
31+
public IndexFreeQueryEngine(LocalDocumentsView localDocumentsView, QueryCache queryCache) {
32+
this.localDocumentsView = localDocumentsView;
33+
this.queryCache = queryCache;
34+
}
35+
36+
@Override
37+
public ImmutableSortedMap<DocumentKey, Document> getDocumentsMatchingQuery(
38+
Query query, @Nullable QueryData queryData) {
39+
if (isSynced(queryData) && !matchesAllDocuments(query)) {
40+
// Retrieve all results for documents that were updated since the last remote snapshot.
41+
ImmutableSortedMap<DocumentKey, Document> docs =
42+
localDocumentsView.getDocumentsMatchingQuery(query, queryData.getSnapshotVersion());
43+
44+
// Merge with the documents that matched the query per the last remote snapshot.
45+
ImmutableSortedSet<DocumentKey> remoteKeys =
46+
queryCache.getMatchingKeysForTargetId(queryData.getTargetId());
47+
ImmutableSortedMap<DocumentKey, MaybeDocument> previousResults =
48+
localDocumentsView.getDocuments(remoteKeys);
49+
for (Map.Entry<DocumentKey, MaybeDocument> entry : previousResults) {
50+
MaybeDocument maybeDoc = entry.getValue();
51+
// Apply the query filter since previously matching documents do not necessarily still
52+
// match the query.
53+
if (maybeDoc instanceof Document && query.matches((Document) maybeDoc)) {
54+
docs = docs.insert(entry.getKey(), (Document) maybeDoc);
55+
}
56+
}
57+
return docs;
58+
} else {
59+
return localDocumentsView.getDocumentsMatchingQuery(query, SnapshotVersion.NONE);
60+
}
61+
}
62+
63+
@Override
64+
public void handleDocumentChange(MaybeDocument oldDocument, MaybeDocument newDocument) {
65+
// No indexes to update.
66+
}
67+
68+
private boolean isSynced(@Nullable QueryData queryData) {
69+
return queryData != null && queryData.isSynced();
70+
}
71+
72+
private static boolean matchesAllDocuments(Query query) {
73+
return query.getFilters().isEmpty()
74+
&& !query.hasLimit()
75+
&& query.getEndAt() == null
76+
&& query.getStartAt() == null;
77+
}
78+
}

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@
4040
* mutations in the MutationQueue to the RemoteDocumentCache.
4141
*/
4242
// TODO: Turn this into the UnifiedDocumentCache / whatever.
43-
final class LocalDocumentsView {
43+
class LocalDocumentsView {
4444

4545
private final RemoteDocumentCache remoteDocumentCache;
4646
private final MutationQueue mutationQueue;
Lines changed: 216 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,216 @@
1+
// Copyright 2019 Google LLC
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
package com.google.firebase.firestore.local;
16+
17+
import static com.google.firebase.firestore.testutil.TestUtil.doc;
18+
import static com.google.firebase.firestore.testutil.TestUtil.filter;
19+
import static com.google.firebase.firestore.testutil.TestUtil.map;
20+
import static com.google.firebase.firestore.testutil.TestUtil.query;
21+
import static com.google.firebase.firestore.testutil.TestUtil.values;
22+
import static com.google.firebase.firestore.testutil.TestUtil.version;
23+
import static java.util.Arrays.asList;
24+
import static org.junit.Assert.assertEquals;
25+
import static org.mockito.ArgumentMatchers.any;
26+
import static org.mockito.ArgumentMatchers.anyInt;
27+
import static org.mockito.Mockito.doAnswer;
28+
import static org.mockito.Mockito.doThrow;
29+
30+
import com.google.firebase.database.collection.ImmutableSortedMap;
31+
import com.google.firebase.database.collection.ImmutableSortedSet;
32+
import com.google.firebase.firestore.core.Query;
33+
import com.google.firebase.firestore.model.Document;
34+
import com.google.firebase.firestore.model.DocumentCollections;
35+
import com.google.firebase.firestore.model.DocumentKey;
36+
import com.google.firebase.firestore.model.MaybeDocument;
37+
import com.google.firebase.firestore.model.SnapshotVersion;
38+
import com.google.protobuf.ByteString;
39+
import java.util.HashMap;
40+
import java.util.Map;
41+
import org.junit.Before;
42+
import org.junit.Test;
43+
import org.junit.runner.RunWith;
44+
import org.mockito.Mock;
45+
import org.mockito.MockitoAnnotations;
46+
import org.robolectric.RobolectricTestRunner;
47+
import org.robolectric.annotation.Config;
48+
49+
@RunWith(RobolectricTestRunner.class)
50+
@Config(manifest = Config.NONE)
51+
public class IndexFreeQueryEngineTest {
52+
53+
private static final Document MATCHING_DOC_A =
54+
doc("coll/a", 1, map("matches", true), Document.DocumentState.SYNCED);
55+
private static final Document NON_MATCHING_DOC_A =
56+
doc("coll/a", 1, map("matches", false), Document.DocumentState.SYNCED);
57+
private static final Document MATCHING_DOC_B =
58+
doc("coll/b", 1, map("matches", true), Document.DocumentState.SYNCED);
59+
private static final Document NON_MATCHING_DOC_B =
60+
doc("coll/b", 1, map("matches", false), Document.DocumentState.SYNCED);
61+
private static final Document UPDATED_MATCHING_DOC_B =
62+
doc("coll/b", 11, map("matches", true), Document.DocumentState.SYNCED);
63+
64+
private static final int TEST_TARGET_ID = 1;
65+
66+
private final Map<Integer, ImmutableSortedSet<DocumentKey>> keysByTarget = new HashMap<>();
67+
private final Map<DocumentKey, Document> documentsInQueryByKey = new HashMap<>();
68+
private final Map<DocumentKey, Document> documentsUpdatedSinceQueryByKey = new HashMap<>();
69+
70+
@Mock private LocalDocumentsView localDocumentsView;
71+
@Mock private QueryCache queryCache;
72+
private QueryEngine queryEngine;
73+
74+
@Before
75+
public void setUp() {
76+
MockitoAnnotations.initMocks(this);
77+
78+
keysByTarget.clear();
79+
documentsInQueryByKey.clear();
80+
documentsUpdatedSinceQueryByKey.clear();
81+
82+
queryEngine = new IndexFreeQueryEngine(localDocumentsView, queryCache);
83+
84+
doAnswer(
85+
getMatchingKeysForTargetIdInvocation -> {
86+
int targetId = getMatchingKeysForTargetIdInvocation.getArgument(0);
87+
return keysByTarget.get(targetId);
88+
})
89+
.when(queryCache)
90+
.getMatchingKeysForTargetId(anyInt());
91+
92+
doAnswer(
93+
getDocumentsInvocation -> {
94+
Iterable<DocumentKey> keys = getDocumentsInvocation.getArgument(0);
95+
96+
ImmutableSortedMap<DocumentKey, MaybeDocument> docs =
97+
DocumentCollections.emptyMaybeDocumentMap();
98+
for (DocumentKey key : keys) {
99+
docs = docs.insert(key, documentsInQueryByKey.get(key));
100+
}
101+
102+
return docs;
103+
})
104+
.when(localDocumentsView)
105+
.getDocuments(any());
106+
107+
doAnswer(
108+
getDocumentsMatchingQueryInvocation -> {
109+
Query query = getDocumentsMatchingQueryInvocation.getArgument(0);
110+
SnapshotVersion snapshotVersion = getDocumentsMatchingQueryInvocation.getArgument(1);
111+
112+
ImmutableSortedMap<DocumentKey, MaybeDocument> matchingDocs =
113+
DocumentCollections.emptyMaybeDocumentMap();
114+
115+
for (Document doc : documentsUpdatedSinceQueryByKey.values()) {
116+
if (query.matches(doc) && doc.getVersion().compareTo(snapshotVersion) >= 0) {
117+
matchingDocs = matchingDocs.insert(doc.getKey(), doc);
118+
}
119+
}
120+
121+
return matchingDocs;
122+
})
123+
.when(localDocumentsView)
124+
.getDocumentsMatchingQuery(any(), any());
125+
}
126+
127+
private void addExistingResult(Document doc) {
128+
ImmutableSortedSet<DocumentKey> currentKeys = keysByTarget.get(TEST_TARGET_ID);
129+
130+
if (currentKeys == null) {
131+
currentKeys = DocumentKey.emptyKeySet().insert(doc.getKey());
132+
} else {
133+
currentKeys = currentKeys.insert(doc.getKey());
134+
}
135+
136+
keysByTarget.put(TEST_TARGET_ID, currentKeys);
137+
documentsInQueryByKey.put(doc.getKey(), doc);
138+
}
139+
140+
private void addUpdatedResult(Document doc) {
141+
documentsUpdatedSinceQueryByKey.put(doc.getKey(), doc);
142+
}
143+
144+
@Test
145+
public void usesTargetMappingForInitialResults() {
146+
Query query = query("coll").filter(filter("matches", "==", true));
147+
QueryData queryData = queryData(query, true);
148+
149+
addExistingResult(MATCHING_DOC_A);
150+
addExistingResult(MATCHING_DOC_B);
151+
152+
ImmutableSortedMap<DocumentKey, Document> docs =
153+
queryEngine.getDocumentsMatchingQuery(query, queryData);
154+
assertEquals(asList(MATCHING_DOC_A, MATCHING_DOC_B), values(docs));
155+
}
156+
157+
@Test
158+
public void filtersNonMatchingInitialResults() {
159+
Query query = query("coll").filter(filter("matches", "==", true));
160+
QueryData queryData = queryData(query, true);
161+
162+
addExistingResult(NON_MATCHING_DOC_A);
163+
addExistingResult(MATCHING_DOC_B);
164+
165+
ImmutableSortedMap<DocumentKey, Document> docs =
166+
queryEngine.getDocumentsMatchingQuery(query, queryData);
167+
assertEquals(asList(MATCHING_DOC_B), values(docs));
168+
}
169+
170+
@Test
171+
public void includesChangesSinceInitialResults() {
172+
Query query = query("coll").filter(filter("matches", "==", true));
173+
QueryData originalQueryData = queryData(query, true);
174+
175+
addExistingResult(MATCHING_DOC_A);
176+
addExistingResult(NON_MATCHING_DOC_B);
177+
178+
ImmutableSortedMap<DocumentKey, Document> docs =
179+
queryEngine.getDocumentsMatchingQuery(query, originalQueryData);
180+
assertEquals(asList(MATCHING_DOC_A), values(docs));
181+
182+
addUpdatedResult(UPDATED_MATCHING_DOC_B);
183+
184+
docs = queryEngine.getDocumentsMatchingQuery(query, originalQueryData);
185+
assertEquals(asList(MATCHING_DOC_A, UPDATED_MATCHING_DOC_B), values(docs));
186+
}
187+
188+
@Test
189+
public void doesNotUseInitialResultsForNonSyncedQuery() {
190+
doThrow(AssertionError.class).when(queryCache).getMatchingKeysForTargetId(anyInt());
191+
192+
Query query = query("coll").filter(filter("matches", "==", true));
193+
QueryData queryData = queryData(query, false);
194+
195+
ImmutableSortedMap<DocumentKey, Document> docs =
196+
queryEngine.getDocumentsMatchingQuery(query, queryData);
197+
assertEquals(asList(), values(docs));
198+
}
199+
200+
@Test
201+
public void doesNotUseInitialResultsForUnfilteredCollectionQuery() {
202+
doThrow(AssertionError.class).when(queryCache).getMatchingKeysForTargetId(anyInt());
203+
204+
Query query = query("coll");
205+
QueryData queryData = queryData(query, true);
206+
207+
ImmutableSortedMap<DocumentKey, Document> docs =
208+
queryEngine.getDocumentsMatchingQuery(query, queryData);
209+
assertEquals(asList(), values(docs));
210+
}
211+
212+
private QueryData queryData(Query query, boolean synced) {
213+
return new QueryData(
214+
query, TEST_TARGET_ID, 1, synced, QueryPurpose.LISTEN, version(10), ByteString.EMPTY);
215+
}
216+
}

0 commit comments

Comments
 (0)