Skip to content

Commit eff0bd7

Browse files
Run one instead of n queries
1 parent c30a15a commit eff0bd7

File tree

2 files changed

+90
-66
lines changed

2 files changed

+90
-66
lines changed

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

Lines changed: 63 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
import static com.google.firebase.firestore.util.Assert.fail;
1818
import static com.google.firebase.firestore.util.Assert.hardAssert;
1919

20+
import android.text.TextUtils;
2021
import androidx.annotation.Nullable;
2122
import com.google.firebase.firestore.core.Bound;
2223
import com.google.firebase.firestore.core.FieldFilter;
@@ -134,8 +135,8 @@ public void addIndexEntries(Document document) {
134135
fieldIndex);
135136
}
136137

137-
List<byte[]> encodeValues = encodeDocumentValues(fieldIndex, values);
138-
for (byte[] encoded : encodeValues) {
138+
Object[] encodeValues = encodeDocumentValues(fieldIndex, values);
139+
for (Object encoded : encodeValues) {
139140
// TODO(indexing): Handle different values for different users
140141
db.execute(
141142
"INSERT OR IGNORE INTO index_entries ("
@@ -176,8 +177,6 @@ public Set<DocumentKey> getDocumentsMatchingTarget(Target target) {
176177
if (fieldIndex == null) return null;
177178

178179
Bound lowerBound = target.getLowerBound(fieldIndex);
179-
String lowerBoundOp = lowerBound.isBefore() ? ">=" : ">"; // `startAt()` versus `startAfter()`
180-
181180
@Nullable Bound upperBound = target.getUpperBound(fieldIndex);
182181

183182
if (Logger.isDebugEnabled()) {
@@ -191,49 +190,59 @@ public Set<DocumentKey> getDocumentsMatchingTarget(Target target) {
191190
}
192191

193192
Set<DocumentKey> result = new HashSet<>();
193+
BindArgs bindArgs;
194194

195+
Object[] lowerBoundValues = encodeTargetValues(fieldIndex, target, lowerBound.getPosition());
196+
String lowerBoundOp = lowerBound.isBefore() ? ">=" : ">"; // `startAt()` versus `startAfter()`
195197
if (upperBound != null) {
196-
List<byte[]> lowerBoundValues =
197-
encodeTargetValues(fieldIndex, target, lowerBound.getPosition());
198-
List<byte[]> upperBoundValues =
199-
encodeTargetValues(fieldIndex, target, upperBound.getPosition());
200-
201-
hardAssert(
202-
lowerBoundValues.size() == upperBoundValues.size(),
203-
"Expected upper and lower bound size to match");
204-
198+
Object[] upperBoundValues = encodeTargetValues(fieldIndex, target, upperBound.getPosition());
205199
String upperBoundOp = upperBound.isBefore() ? "<" : "<="; // `endBefore()` versus `endAt()`
206-
207-
// TODO(indexing): To avoid reading the same documents multiple times, we should ideally only
208-
// send one query that combines all clauses.
209-
// TODO(indexing): Add limit handling
210-
for (int i = 0; i < lowerBoundValues.size(); ++i) {
211-
db.query(
212-
String.format(
213-
"SELECT document_name from index_entries WHERE index_id = ? AND index_value %s ? AND index_value %s ?",
214-
lowerBoundOp, upperBoundOp))
215-
.binding(fieldIndex.getIndexId(), lowerBoundValues.get(i), upperBoundValues.get(i))
216-
.forEach(
217-
row -> result.add(DocumentKey.fromPath(ResourcePath.fromString(row.getString(0)))));
218-
}
200+
bindArgs = generateBindArgs(lowerBoundValues, lowerBoundOp, upperBoundValues, upperBoundOp);
219201
} else {
220-
List<byte[]> lowerBoundValues =
221-
encodeTargetValues(fieldIndex, target, lowerBound.getPosition());
222-
for (byte[] lowerBoundValue : lowerBoundValues) {
223-
db.query(
224-
String.format(
225-
"SELECT document_name from index_entries WHERE index_id = ? AND index_value %s ?",
226-
lowerBoundOp))
227-
.binding(fieldIndex.getIndexId(), lowerBoundValue)
228-
.forEach(
229-
row -> result.add(DocumentKey.fromPath(ResourcePath.fromString(row.getString(0)))));
230-
}
202+
bindArgs = generateBindArgs(lowerBoundValues, lowerBoundOp);
231203
}
232204

205+
db.query(
206+
String.format(
207+
"SELECT document_name from index_entries WHERE index_id = ? AND (%s)",
208+
bindArgs.sql))
209+
.binding(fieldIndex.getIndexId(), bindArgs.bindArgs)
210+
.forEach(
211+
row -> result.add(DocumentKey.fromPath(ResourcePath.fromString(row.getString(0)))));
212+
// TODO(indexing): Add limit handling
213+
233214
Logger.debug(TAG, "Index scan returned %s documents", result.size());
234215
return result;
235216
}
236217

218+
/** Returns a SQL filter that concatenates all {@code value} into a disjunction. */
219+
private BindArgs generateBindArgs(Object[] values, String op) {
220+
String[] filters = new String[values.length];
221+
for (int i = 0; i < values.length; ++i) {
222+
filters[i] = String.format("index_value %s ?", op);
223+
}
224+
return new BindArgs(TextUtils.join(" OR ", filters), values);
225+
}
226+
227+
/**
228+
* Returns a SQL filter that combines each element from the left list with each element from the
229+
* right list and returns all combinations (e.g. `(left1 AND right1) OR (left1 AND right2) ...`).
230+
*/
231+
private BindArgs generateBindArgs(Object[] left, String leftOp, Object[] right, String rightOp) {
232+
String[] filters = new String[left.length * right.length];
233+
Object[] bingArgs = new Object[left.length * right.length * 2];
234+
int i = 0;
235+
for (Object value1 : left) {
236+
for (Object value2 : right) {
237+
filters[i] = String.format("index_value %s ? AND index_value %s ?", leftOp, rightOp);
238+
bingArgs[i * 2] = value1;
239+
bingArgs[i * 2 + 1] = value2;
240+
++i;
241+
}
242+
}
243+
return new BindArgs(TextUtils.join(" OR ", filters), bingArgs);
244+
}
245+
237246
/**
238247
* Returns an index that can be used to serve the provided target. Returns {@code null} if no
239248
* index is configured.
@@ -278,7 +287,7 @@ public Set<DocumentKey> getDocumentsMatchingTarget(Target target) {
278287
* Encodes the given field values according to the specification in {@code fieldIndex}. For
279288
* CONTAINS indices, a list of possible values is returned.
280289
*/
281-
private List<byte[]> encodeDocumentValues(FieldIndex fieldIndex, List<Value> values) {
290+
private Object[] encodeDocumentValues(FieldIndex fieldIndex, List<Value> values) {
282291
List<IndexByteEncoder> encoders = new ArrayList<>();
283292
encoders.add(new IndexByteEncoder());
284293
for (int i = 0; i < fieldIndex.segmentCount(); ++i) {
@@ -302,8 +311,7 @@ private List<byte[]> encodeDocumentValues(FieldIndex fieldIndex, List<Value> val
302311
* Encodes the given field values according to the specification in {@code target}. For IN and
303312
* ArrayContainsAny queries, a list of possible values is returned.
304313
*/
305-
private List<byte[]> encodeTargetValues(
306-
FieldIndex fieldIndex, Target target, List<Value> values) {
314+
private Object[] encodeTargetValues(FieldIndex fieldIndex, Target target, List<Value> values) {
307315
List<IndexByteEncoder> encoders = new ArrayList<>();
308316
encoders.add(new IndexByteEncoder());
309317
for (int i = 0; i < fieldIndex.segmentCount(); ++i) {
@@ -321,10 +329,10 @@ private List<byte[]> encodeTargetValues(
321329
}
322330

323331
/** Returns the byte representation for all encoders. */
324-
private List<byte[]> getEncodedBytes(List<IndexByteEncoder> encoders) {
325-
List<byte[]> result = new ArrayList<>();
326-
for (IndexByteEncoder encoder : encoders) {
327-
result.add(encoder.getEncodedBytes());
332+
private Object[] getEncodedBytes(List<IndexByteEncoder> encoders) {
333+
Object[] result = new Object[encoders.size()];
334+
for (int i = 0; i < encoders.size(); ++i) {
335+
result[i] = encoders.get(i).getEncodedBytes();
328336
}
329337
return result;
330338
}
@@ -368,4 +376,15 @@ private boolean isMultiValueFilter(Target target, FieldPath fieldPath) {
368376
private byte[] encodeFieldIndex(FieldIndex fieldIndex) {
369377
return serializer.encodeFieldIndex(fieldIndex).toByteArray();
370378
}
379+
380+
/** Stores a SQL statement of filters and their corresponding bind arguments. */
381+
static class BindArgs {
382+
final String sql;
383+
final Object[] bindArgs;
384+
385+
BindArgs(String sql, Object[] bindArgs) {
386+
this.sql = sql;
387+
this.bindArgs = bindArgs;
388+
}
389+
}
371390
}

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

Lines changed: 27 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -374,7 +374,7 @@ SQLiteStatement prepare(String sql) {
374374
*/
375375
int execute(SQLiteStatement statement, Object... args) {
376376
statement.clearBindings();
377-
bind(statement, args);
377+
bind(statement, 0, args);
378378
return statement.executeUpdateDelete();
379379
}
380380

@@ -442,7 +442,7 @@ static class Query {
442442
*
443443
* @return this Query object, for chaining.
444444
*/
445-
Query binding(Object... args) {
445+
Query binding(Object arg, Object... args) {
446446
// This is gross, but the best way to preserve both the readability of the caller (since
447447
// values don't have be arbitrarily converted to Strings) and allows BLOBs to be used as
448448
// bind arguments.
@@ -458,7 +458,8 @@ Query binding(Object... args) {
458458

459459
cursorFactory =
460460
(db1, masterQuery, editTable, query) -> {
461-
bind(query, args);
461+
bind(query, /* offset= */ 0, arg);
462+
bind(query, /* offset= */ 1, args);
462463
return new SQLiteCursor(masterQuery, editTable, query);
463464
};
464465
return this;
@@ -684,8 +685,14 @@ int getSubqueriesPerformed() {
684685
}
685686
}
686687

688+
/** Binds the given arguments to the given SQLite statement or query. */
689+
private static void bind(SQLiteProgram program, int offset, Object[] bindArgs) {
690+
for (int i = 0; i < bindArgs.length; i++) {
691+
bind(program, i + offset, bindArgs[i]);
692+
}
693+
}
687694
/**
688-
* Binds the given arguments to the given SQLite statement or query.
695+
* Binds a single argument to the given SQLite statement or query.
689696
*
690697
* <p>This method helps work around the fact that all of the querying methods on SQLiteDatabase
691698
* take an array of strings for bind arguments. Most values can be straightforwardly converted to
@@ -695,24 +702,22 @@ int getSubqueriesPerformed() {
695702
* This method bridges the gap by examining the types of the bindArgs and calling to the
696703
* appropriate bind method on the program.
697704
*/
698-
private static void bind(SQLiteProgram program, Object[] bindArgs) {
699-
for (int i = 0; i < bindArgs.length; i++) {
700-
Object arg = bindArgs[i];
701-
if (arg == null) {
702-
program.bindNull(i + 1);
703-
} else if (arg instanceof String) {
704-
program.bindString(i + 1, (String) arg);
705-
} else if (arg instanceof Integer) {
706-
program.bindLong(i + 1, (Integer) arg);
707-
} else if (arg instanceof Long) {
708-
program.bindLong(i + 1, (Long) arg);
709-
} else if (arg instanceof Double) {
710-
program.bindDouble(i + 1, (Double) arg);
711-
} else if (arg instanceof byte[]) {
712-
program.bindBlob(i + 1, (byte[]) arg);
713-
} else {
714-
throw fail("Unknown argument %s of type %s", arg, arg.getClass());
715-
}
705+
private static void bind(SQLiteProgram program, int pos, Object bindArg) {
706+
Object arg = bindArg;
707+
if (arg == null) {
708+
program.bindNull(pos + 1);
709+
} else if (arg instanceof String) {
710+
program.bindString(pos + 1, (String) arg);
711+
} else if (arg instanceof Integer) {
712+
program.bindLong(pos + 1, (Integer) arg);
713+
} else if (arg instanceof Long) {
714+
program.bindLong(pos + 1, (Long) arg);
715+
} else if (arg instanceof Double) {
716+
program.bindDouble(pos + 1, (Double) arg);
717+
} else if (arg instanceof byte[]) {
718+
program.bindBlob(pos + 1, (byte[]) arg);
719+
} else {
720+
throw fail("Unknown argument %s of type %s", arg, arg.getClass());
716721
}
717722
}
718723
}

0 commit comments

Comments
 (0)