Skip to content

Commit 45dd9ff

Browse files
Run one instead of n queries (#2982)
1 parent 90efb98 commit 45dd9ff

File tree

2 files changed

+82
-47
lines changed

2 files changed

+82
-47
lines changed

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

Lines changed: 69 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616

1717
import static com.google.firebase.firestore.util.Assert.fail;
1818
import static com.google.firebase.firestore.util.Assert.hardAssert;
19+
import static com.google.firebase.firestore.util.Util.repeatSequence;
1920

2021
import androidx.annotation.Nullable;
2122
import com.google.firebase.firestore.core.Bound;
@@ -96,8 +97,6 @@ public void addFieldIndex(FieldIndex index) {
9697
db.query("SELECT MAX(index_id) FROM index_configuration")
9798
.firstValue(input -> input.isNull(0) ? 0 : input.getInt(0));
9899

99-
// TODO(indexing): Properly dedupe indices to avoid duplicate index entries (by comparing
100-
// collection_group+index_proto)
101100
db.execute(
102101
"INSERT OR IGNORE INTO index_configuration ("
103102
+ "index_id, "
@@ -145,8 +144,8 @@ public void addIndexEntries(Document document) {
145144
fieldIndex);
146145
}
147146

148-
List<byte[]> encodeValues = encodeDocumentValues(fieldIndex, values);
149-
for (byte[] encoded : encodeValues) {
147+
Object[] encodeValues = encodeDocumentValues(fieldIndex, values);
148+
for (Object encoded : encodeValues) {
150149
// TODO(indexing): Handle different values for different users
151150
db.execute(
152151
"INSERT OR IGNORE INTO index_entries ("
@@ -187,12 +186,10 @@ public Set<DocumentKey> getDocumentsMatchingTarget(Target target) {
187186
if (fieldIndex == null) return null;
188187

189188
Bound lowerBound = target.getLowerBound(fieldIndex);
190-
String lowerBoundOp = lowerBound.isInclusive() ? ">=" : ">";
191-
192189
@Nullable Bound upperBound = target.getUpperBound(fieldIndex);
193190

194191
if (Logger.isDebugEnabled()) {
195-
Logger.warn(
192+
Logger.debug(
196193
TAG,
197194
"Using index '%s' to execute '%s' (Lower bound: %s, Upper bound: %s)",
198195
fieldIndex,
@@ -202,49 +199,75 @@ public Set<DocumentKey> getDocumentsMatchingTarget(Target target) {
202199
}
203200

204201
Set<DocumentKey> result = new HashSet<>();
202+
SQLitePersistence.Query query;
205203

204+
Object[] lowerBoundValues = encodeTargetValues(fieldIndex, target, lowerBound.getPosition());
205+
String lowerBoundOp = lowerBound.isInclusive() ? ">=" : ">";
206206
if (upperBound != null) {
207-
List<byte[]> lowerBoundValues =
208-
encodeTargetValues(fieldIndex, target, lowerBound.getPosition());
209-
List<byte[]> upperBoundValues =
210-
encodeTargetValues(fieldIndex, target, upperBound.getPosition());
211-
212-
hardAssert(
213-
lowerBoundValues.size() == upperBoundValues.size(),
214-
"Expected upper and lower bound size to match");
215-
207+
Object[] upperBoundValues = encodeTargetValues(fieldIndex, target, upperBound.getPosition());
216208
String upperBoundOp = upperBound.isInclusive() ? "<=" : "<";
217-
218-
// TODO(indexing): To avoid reading the same documents multiple times, we should ideally only
219-
// send one query that combines all clauses.
220-
// TODO(indexing): Add limit handling
221-
for (int i = 0; i < lowerBoundValues.size(); ++i) {
222-
db.query(
223-
String.format(
224-
"SELECT document_name from index_entries WHERE index_id = ? AND index_value %s ? AND index_value %s ?",
225-
lowerBoundOp, upperBoundOp))
226-
.binding(fieldIndex.getIndexId(), lowerBoundValues.get(i), upperBoundValues.get(i))
227-
.forEach(
228-
row -> result.add(DocumentKey.fromPath(ResourcePath.fromString(row.getString(0)))));
229-
}
209+
query =
210+
generateQuery(
211+
fieldIndex.getIndexId(),
212+
lowerBoundValues,
213+
lowerBoundOp,
214+
upperBoundValues,
215+
upperBoundOp);
230216
} else {
231-
List<byte[]> lowerBoundValues =
232-
encodeTargetValues(fieldIndex, target, lowerBound.getPosition());
233-
for (byte[] lowerBoundValue : lowerBoundValues) {
234-
db.query(
235-
String.format(
236-
"SELECT document_name from index_entries WHERE index_id = ? AND index_value %s ?",
237-
lowerBoundOp))
238-
.binding(fieldIndex.getIndexId(), lowerBoundValue)
239-
.forEach(
240-
row -> result.add(DocumentKey.fromPath(ResourcePath.fromString(row.getString(0)))));
241-
}
217+
query = generateQuery(fieldIndex.getIndexId(), lowerBoundValues, lowerBoundOp);
242218
}
243219

220+
query.forEach(
221+
row -> result.add(DocumentKey.fromPath(ResourcePath.fromString(row.getString(0)))));
222+
223+
// TODO(indexing): Add limit handling
224+
244225
Logger.debug(TAG, "Index scan returned %s documents", result.size());
245226
return result;
246227
}
247228

229+
/** Returns a SQL query on 'index_entries' that unions all bounds. */
230+
private SQLitePersistence.Query generateQuery(int indexId, Object[] bounds, String op) {
231+
String statement =
232+
String.format(
233+
"SELECT document_name FROM index_entries WHERE index_id = ? AND index_value %s ?", op);
234+
String sql = repeatSequence(statement, bounds.length, " UNION ");
235+
236+
Object[] bingArgs = new Object[bounds.length * 2];
237+
for (int i = 0; i < bounds.length; ++i) {
238+
bingArgs[i * 2] = indexId;
239+
bingArgs[i * 2 + 1] = bounds[i];
240+
}
241+
242+
return db.query(sql).binding(bingArgs);
243+
}
244+
245+
/** Returns a SQL query on 'index_entries' that unions all bounds. */
246+
private SQLitePersistence.Query generateQuery(
247+
int indexId,
248+
Object[] lowerBounds,
249+
String lowerBoundOp,
250+
Object[] upperBounds,
251+
String upperBoundOp) {
252+
String statement =
253+
String.format(
254+
"SELECT document_name FROM index_entries WHERE index_id = ? AND index_value %s ? AND index_value %s ?",
255+
lowerBoundOp, upperBoundOp);
256+
String sql = repeatSequence(statement, lowerBounds.length * upperBounds.length, " UNION ");
257+
258+
Object[] bingArgs = new Object[lowerBounds.length * upperBounds.length * 3];
259+
int i = 0;
260+
for (Object value1 : lowerBounds) {
261+
for (Object value2 : upperBounds) {
262+
bingArgs[i++] = indexId;
263+
bingArgs[i++] = value1;
264+
bingArgs[i++] = value2;
265+
}
266+
}
267+
268+
return db.query(sql).binding(bingArgs);
269+
}
270+
248271
/**
249272
* Returns an index that can be used to serve the provided target. Returns {@code null} if no
250273
* index is configured.
@@ -293,7 +316,7 @@ public Set<DocumentKey> getDocumentsMatchingTarget(Target target) {
293316
* Encodes the given field values according to the specification in {@code fieldIndex}. For
294317
* CONTAINS indices, a list of possible values is returned.
295318
*/
296-
private List<byte[]> encodeDocumentValues(FieldIndex fieldIndex, List<Value> values) {
319+
private Object[] encodeDocumentValues(FieldIndex fieldIndex, List<Value> values) {
297320
List<IndexByteEncoder> encoders = new ArrayList<>();
298321
encoders.add(new IndexByteEncoder());
299322
for (int i = 0; i < fieldIndex.segmentCount(); ++i) {
@@ -317,8 +340,7 @@ private List<byte[]> encodeDocumentValues(FieldIndex fieldIndex, List<Value> val
317340
* Encodes the given field values according to the specification in {@code target}. For IN and
318341
* ArrayContainsAny queries, a list of possible values is returned.
319342
*/
320-
private List<byte[]> encodeTargetValues(
321-
FieldIndex fieldIndex, Target target, List<Value> values) {
343+
private Object[] encodeTargetValues(FieldIndex fieldIndex, Target target, List<Value> values) {
322344
List<IndexByteEncoder> encoders = new ArrayList<>();
323345
encoders.add(new IndexByteEncoder());
324346
for (int i = 0; i < fieldIndex.segmentCount(); ++i) {
@@ -336,10 +358,10 @@ private List<byte[]> encodeTargetValues(
336358
}
337359

338360
/** Returns the byte representation for all encoders. */
339-
private List<byte[]> getEncodedBytes(List<IndexByteEncoder> encoders) {
340-
List<byte[]> result = new ArrayList<>();
341-
for (IndexByteEncoder encoder : encoders) {
342-
result.add(encoder.getEncodedBytes());
361+
private Object[] getEncodedBytes(List<IndexByteEncoder> encoders) {
362+
Object[] result = new Object[encoders.size()];
363+
for (int i = 0; i < encoders.size(); ++i) {
364+
result[i] = encoders.get(i).getEncodedBytes();
343365
}
344366
return result;
345367
}

firebase-firestore/src/main/java/com/google/firebase/firestore/util/Util.java

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -234,4 +234,17 @@ public static int compareByteStrings(ByteString left, ByteString right) {
234234
}
235235
return Util.compareIntegers(left.size(), right.size());
236236
}
237+
238+
public static String repeatSequence(CharSequence sequence, int count, CharSequence delimiter) {
239+
if (count == 0) {
240+
return "";
241+
}
242+
final StringBuilder sb = new StringBuilder();
243+
sb.append(sequence);
244+
for (int i = 1; i < count; i++) {
245+
sb.append(delimiter);
246+
sb.append(sequence);
247+
}
248+
return sb.toString();
249+
}
237250
}

0 commit comments

Comments
 (0)