Skip to content

Commit efc9fb7

Browse files
Index-free: Use different SQL for non-index free queries (#726)
1 parent 0df060a commit efc9fb7

File tree

2 files changed

+61
-45
lines changed

2 files changed

+61
-45
lines changed

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

Lines changed: 52 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -155,48 +155,59 @@ public ImmutableSortedMap<DocumentKey, Document> getAllDocumentsMatchingQuery(
155155
(ImmutableSortedMap<DocumentKey, Document>[])
156156
new ImmutableSortedMap[] {DocumentCollections.emptyDocumentMap()};
157157

158+
SQLitePersistence.Query sqlQuery;
159+
if (sinceReadTime.equals(SnapshotVersion.NONE)) {
160+
sqlQuery =
161+
db.query("SELECT path, contents FROM remote_documents WHERE path >= ? AND path < ?")
162+
.binding(prefixPath, prefixSuccessorPath);
163+
} else {
164+
// Execute an index-free query and filter by read time. This is safe since all document
165+
// changes to queries that have a lastLimboFreeSnapshotVersion (`sinceReadTime`) have a read
166+
// time set.
167+
sqlQuery =
168+
db.query(
169+
"SELECT path, contents FROM remote_documents WHERE path >= ? AND path < ?"
170+
+ "AND (read_time_seconds > ? OR (read_time_seconds = ? AND read_time_nanos > ?))")
171+
.binding(
172+
prefixPath,
173+
prefixSuccessorPath,
174+
readTime.getSeconds(),
175+
readTime.getSeconds(),
176+
readTime.getNanoseconds());
177+
}
178+
158179
int rowsProcessed =
159-
db.query(
160-
"SELECT path, contents FROM remote_documents WHERE path >= ? AND path < ? "
161-
+ "AND (read_time_seconds IS NULL OR read_time_seconds > ? "
162-
+ "OR (read_time_seconds = ? AND read_time_nanos > ?))")
163-
.binding(
164-
prefixPath,
165-
prefixSuccessorPath,
166-
readTime.getSeconds(),
167-
readTime.getSeconds(),
168-
readTime.getNanoseconds())
169-
.forEach(
170-
row -> {
171-
// TODO: Actually implement a single-collection query
172-
//
173-
// The query is actually returning any path that starts with the query path prefix
174-
// which may include documents in subcollections. For example, a query on 'rooms'
175-
// will return rooms/abc/messages/xyx but we shouldn't match it. Fix this by
176-
// discarding rows with document keys more than one segment longer than the query
177-
// path.
178-
ResourcePath path = EncodedPath.decodeResourcePath(row.getString(0));
179-
if (path.length() != immediateChildrenPathLength) {
180-
return;
181-
}
182-
183-
byte[] rawDocument = row.getBlob(1);
184-
185-
// Since scheduling background tasks incurs overhead, we only dispatch to a
186-
// background thread if there are still some documents remaining.
187-
Executor executor = row.isLast() ? Executors.DIRECT_EXECUTOR : backgroundQueue;
188-
executor.execute(
189-
() -> {
190-
MaybeDocument maybeDoc = decodeMaybeDocument(rawDocument);
191-
192-
if (maybeDoc instanceof Document && query.matches((Document) maybeDoc)) {
193-
synchronized (SQLiteRemoteDocumentCache.this) {
194-
matchingDocuments[0] =
195-
matchingDocuments[0].insert(maybeDoc.getKey(), (Document) maybeDoc);
196-
}
197-
}
198-
});
199-
});
180+
sqlQuery.forEach(
181+
row -> {
182+
// TODO: Actually implement a single-collection query
183+
//
184+
// The query is actually returning any path that starts with the query path prefix
185+
// which may include documents in subcollections. For example, a query on 'rooms'
186+
// will return rooms/abc/messages/xyx but we shouldn't match it. Fix this by
187+
// discarding rows with document keys more than one segment longer than the query
188+
// path.
189+
ResourcePath path = EncodedPath.decodeResourcePath(row.getString(0));
190+
if (path.length() != immediateChildrenPathLength) {
191+
return;
192+
}
193+
194+
byte[] rawDocument = row.getBlob(1);
195+
196+
// Since scheduling background tasks incurs overhead, we only dispatch to a
197+
// background thread if there are still some documents remaining.
198+
Executor executor = row.isLast() ? Executors.DIRECT_EXECUTOR : backgroundQueue;
199+
executor.execute(
200+
() -> {
201+
MaybeDocument maybeDoc = decodeMaybeDocument(rawDocument);
202+
203+
if (maybeDoc instanceof Document && query.matches((Document) maybeDoc)) {
204+
synchronized (SQLiteRemoteDocumentCache.this) {
205+
matchingDocuments[0] =
206+
matchingDocuments[0].insert(maybeDoc.getKey(), (Document) maybeDoc);
207+
}
208+
}
209+
});
210+
});
200211

201212
statsCollector.recordRowsRead(STATS_TAG, rowsProcessed);
202213

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

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -423,12 +423,17 @@ public void existingDocumentsRemainReadableAfterIndexFreeMigration() {
423423
new Object[] {encode(path("coll/new")), 0, 3000, createDummyDocument("coll/new")});
424424

425425
SQLiteRemoteDocumentCache remoteDocumentCache = createRemoteDocumentCache();
426+
427+
// Verify that queries with SnapshotVersion.NONE return all results, regardless of whether the
428+
// read time has been set.
426429
ImmutableSortedMap<DocumentKey, com.google.firebase.firestore.model.Document> results =
427-
remoteDocumentCache.getAllDocumentsMatchingQuery(query("coll"), version(2));
430+
remoteDocumentCache.getAllDocumentsMatchingQuery(query("coll"), version(0));
431+
assertResultsContain(results, "coll/existing", "coll/old", "coll/current", "coll/new");
428432

429-
// Verify that queries return the documents that existed prior to the index-free migration, as
430-
// well as any documents with a newer read time than the one passed in.
431-
assertResultsContain(results, "coll/existing", "coll/new");
433+
// Queries that filter by read time only return documents that were written after the index-free
434+
// migration.
435+
results = remoteDocumentCache.getAllDocumentsMatchingQuery(query("coll"), version(2));
436+
assertResultsContain(results, "coll/new");
432437
}
433438

434439
private SQLiteRemoteDocumentCache createRemoteDocumentCache() {

0 commit comments

Comments
 (0)