-
Notifications
You must be signed in to change notification settings - Fork 626
Serve OR queries from the index. #3368
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 7 commits
6d9be9a
6e62711
4860613
893a014
28ac1cb
81b6e16
c829fe1
d65a6e0
4b7858b
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -74,6 +74,18 @@ public interface IndexManager { | |
/** Returns all configured field indexes. */ | ||
Collection<FieldIndex> getFieldIndexes(); | ||
|
||
/** Returns whether we can serve the given target from the index. */ | ||
boolean canServeFromIndex(Target target); | ||
|
||
/** | ||
* Iterates over all field indexes that are used to serve the given target, and returns the least | ||
* recent offset of them all. Asserts that the target can be served from index. | ||
*/ | ||
FieldIndex.IndexOffset getLeastRecentIndexOffset(Target target); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. minOffset? You could alternatively also drop the LeastRecent/Min name altogether and just call this There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think it'd be more clear to keep a wording that signals to the caller that this method will do a There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Done |
||
|
||
/** Returns the lowest offset for the provided index group. */ | ||
FieldIndex.IndexOffset getLeastRecentIndexOffset(Collection<FieldIndex> fieldIndexes); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Can we just pass the collection group? The index manager has all indices in memory. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Also we should probably add a static import for IndexOffset or make IndexOffset a high level class. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Done |
||
|
||
/** | ||
* Returns an index that can be used to serve the provided target. Returns {@code null} if no | ||
* index is configured. | ||
|
@@ -82,7 +94,7 @@ public interface IndexManager { | |
FieldIndex getFieldIndex(Target target); | ||
|
||
/** Returns the documents that match the given target based on the provided index. */ | ||
Set<DocumentKey> getDocumentsMatchingTarget(FieldIndex fieldIndex, Target target); | ||
Set<DocumentKey> getDocumentsMatchingTarget(Target target); | ||
|
||
/** Returns the next collection group to update. Returns {@code null} if no group exists. */ | ||
@Nullable | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -21,11 +21,14 @@ | |
import static com.google.firebase.firestore.util.Util.repeatSequence; | ||
import static java.lang.Math.max; | ||
|
||
import android.text.TextUtils; | ||
import androidx.annotation.Nullable; | ||
import com.google.common.collect.ObjectArrays; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We have a goal to kick our Guava dependency, as the Android build doesn't deal well with version conflicts for popular libraries. Hence, we should not directly use Guava and we eventually want to remove it altogether (it is used by GRPC). There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Done |
||
import com.google.firebase.Timestamp; | ||
import com.google.firebase.database.collection.ImmutableSortedMap; | ||
import com.google.firebase.firestore.auth.User; | ||
import com.google.firebase.firestore.core.Bound; | ||
import com.google.firebase.firestore.core.CompositeFilter; | ||
import com.google.firebase.firestore.core.FieldFilter; | ||
import com.google.firebase.firestore.core.Filter; | ||
import com.google.firebase.firestore.core.Target; | ||
|
@@ -41,10 +44,13 @@ | |
import com.google.firebase.firestore.model.SnapshotVersion; | ||
import com.google.firebase.firestore.model.TargetIndexMatcher; | ||
import com.google.firebase.firestore.util.Logger; | ||
import com.google.firebase.firestore.util.LogicUtils; | ||
import com.google.firestore.admin.v1.Index; | ||
import com.google.firestore.v1.StructuredQuery; | ||
import com.google.firestore.v1.Value; | ||
import com.google.protobuf.InvalidProtocolBufferException; | ||
import java.util.ArrayList; | ||
import java.util.Arrays; | ||
import java.util.Collection; | ||
import java.util.Collections; | ||
import java.util.HashMap; | ||
|
@@ -66,6 +72,12 @@ final class SQLiteIndexManager implements IndexManager { | |
private final LocalSerializer serializer; | ||
private final String uid; | ||
|
||
/** | ||
* Maps from a target to its equivalent list of sub-targets. Each sub-target contains only one | ||
* term from the target's disjunctive normal form (DNF). | ||
*/ | ||
private final Map<Target, List<Target>> targetToDnfSubTargets = new HashMap<>(); | ||
ehsannas marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
/** | ||
* An in-memory copy of the index entries we've already written since the SDK launched. Used to | ||
* avoid re-writing the same entry repeatedly. | ||
|
@@ -272,6 +284,80 @@ public Collection<FieldIndex> getFieldIndexes() { | |
return allIndices; | ||
} | ||
|
||
@Override | ||
public boolean canServeFromIndex(Target target) { | ||
for (Target subTarget : getSubTargets(target)) { | ||
// If any of the sub-queries cannot be served from the index, the target as a whole cannot be | ||
// served from the index. | ||
if (getFieldIndex(subTarget) == null) { | ||
return false; | ||
} | ||
} | ||
return true; | ||
} | ||
|
||
@Override | ||
public FieldIndex.IndexOffset getLeastRecentIndexOffset(Collection<FieldIndex> fieldIndexes) { | ||
hardAssert( | ||
!fieldIndexes.isEmpty(), | ||
"Found empty index group when looking for least recent index offset."); | ||
|
||
Iterator<FieldIndex> it = fieldIndexes.iterator(); | ||
FieldIndex.IndexOffset minOffset = it.next().getIndexState().getOffset(); | ||
int minBatchId = minOffset.getLargestBatchId(); | ||
while (it.hasNext()) { | ||
FieldIndex.IndexOffset newOffset = it.next().getIndexState().getOffset(); | ||
if (newOffset.compareTo(minOffset) < 0) { | ||
minOffset = newOffset; | ||
} | ||
minBatchId = Math.max(newOffset.getLargestBatchId(), minBatchId); | ||
} | ||
|
||
return FieldIndex.IndexOffset.create( | ||
minOffset.getReadTime(), minOffset.getDocumentKey(), minBatchId); | ||
} | ||
|
||
@Override | ||
public FieldIndex.IndexOffset getLeastRecentIndexOffset(Target target) { | ||
hardAssert( | ||
canServeFromIndex(target), | ||
"Cannot find least recent index offset if target cannot be served from index."); | ||
List<FieldIndex> fieldIndexes = new ArrayList<>(); | ||
for (Target subTarget : getSubTargets(target)) { | ||
fieldIndexes.add(getFieldIndex(subTarget)); | ||
} | ||
return getLeastRecentIndexOffset(fieldIndexes); | ||
} | ||
|
||
private List<Target> getSubTargets(Target target) { | ||
if (targetToDnfSubTargets.containsKey(target)) { | ||
return targetToDnfSubTargets.get(target); | ||
} | ||
List<Target> subTargets = new ArrayList<>(); | ||
if (target.getFilters().isEmpty()) { | ||
subTargets.add(target); | ||
} else { | ||
// There is an implicit AND operation between all the filters stored in the target. | ||
List<Filter> dnf = | ||
LogicUtils.DnfTransform( | ||
new CompositeFilter( | ||
target.getFilters(), StructuredQuery.CompositeFilter.Operator.AND)); | ||
for (Filter term : dnf) { | ||
subTargets.add( | ||
new Target( | ||
target.getPath(), | ||
target.getCollectionGroup(), | ||
term.getFilters(), | ||
target.getOrderBy(), | ||
target.getLimit(), | ||
target.getStartAt(), | ||
target.getEndAt())); | ||
} | ||
} | ||
targetToDnfSubTargets.put(target, subTargets); | ||
return subTargets; | ||
} | ||
|
||
/** | ||
* Stores the index in the memoized indexes table and updates {@link #nextIndexToUpdate}, {@link | ||
* #memoizedMaxIndexId} and {@link #memoizedMaxSequenceNumber}. | ||
|
@@ -364,41 +450,60 @@ private SortedSet<IndexEntry> getExistingIndexEntries( | |
} | ||
|
||
@Override | ||
public Set<DocumentKey> getDocumentsMatchingTarget(FieldIndex fieldIndex, Target target) { | ||
public Set<DocumentKey> getDocumentsMatchingTarget(Target target) { | ||
hardAssert(started, "IndexManager not started"); | ||
|
||
@Nullable List<Value> arrayValues = target.getArrayValues(fieldIndex); | ||
@Nullable List<Value> notInValues = target.getNotInValues(fieldIndex); | ||
@Nullable Bound lowerBound = target.getLowerBound(fieldIndex); | ||
@Nullable Bound upperBound = target.getUpperBound(fieldIndex); | ||
|
||
if (Logger.isDebugEnabled()) { | ||
Logger.debug( | ||
TAG, | ||
"Using index '%s' to execute '%s' (Arrays: %s, Lower bound: %s, Upper bound: %s)", | ||
fieldIndex, | ||
target, | ||
arrayValues, | ||
lowerBound, | ||
upperBound); | ||
} | ||
|
||
Object[] lowerBoundEncoded = encodeBound(fieldIndex, target, lowerBound); | ||
String lowerBoundOp = lowerBound != null && lowerBound.isInclusive() ? ">=" : ">"; | ||
Object[] upperBoundEncoded = encodeBound(fieldIndex, target, upperBound); | ||
String upperBoundOp = upperBound != null && upperBound.isInclusive() ? "<=" : "<"; | ||
Object[] notInEncoded = encodeValues(fieldIndex, target, notInValues); | ||
|
||
SQLitePersistence.Query query = | ||
generateQuery( | ||
target, | ||
fieldIndex.getIndexId(), | ||
List<String> subQueries = new ArrayList<>(); | ||
List<Object> bindings = new ArrayList<>(); | ||
|
||
for (Target subTarget : getSubTargets(target)) { | ||
FieldIndex fieldIndex = getFieldIndex(subTarget); | ||
@Nullable List<Value> arrayValues = subTarget.getArrayValues(fieldIndex); | ||
@Nullable List<Value> notInValues = subTarget.getNotInValues(fieldIndex); | ||
@Nullable Bound lowerBound = subTarget.getLowerBound(fieldIndex); | ||
@Nullable Bound upperBound = subTarget.getUpperBound(fieldIndex); | ||
|
||
if (Logger.isDebugEnabled()) { | ||
Logger.debug( | ||
TAG, | ||
"Using index '%s' to execute '%s' (Arrays: %s, Lower bound: %s, Upper bound: %s)", | ||
fieldIndex, | ||
subTarget, | ||
arrayValues, | ||
lowerBoundEncoded, | ||
lowerBoundOp, | ||
upperBoundEncoded, | ||
upperBoundOp, | ||
notInEncoded); | ||
lowerBound, | ||
upperBound); | ||
} | ||
|
||
Object[] lowerBoundEncoded = encodeBound(fieldIndex, subTarget, lowerBound); | ||
String lowerBoundOp = lowerBound != null && lowerBound.isInclusive() ? ">=" : ">"; | ||
Object[] upperBoundEncoded = encodeBound(fieldIndex, subTarget, upperBound); | ||
String upperBoundOp = upperBound != null && upperBound.isInclusive() ? "<=" : "<"; | ||
Object[] notInEncoded = encodeValues(fieldIndex, subTarget, notInValues); | ||
|
||
Object[] subQueryAndBindings = | ||
generateQueryAndBindings( | ||
subTarget, | ||
fieldIndex.getIndexId(), | ||
arrayValues, | ||
lowerBoundEncoded, | ||
lowerBoundOp, | ||
upperBoundEncoded, | ||
upperBoundOp, | ||
notInEncoded); | ||
subQueries.add(String.valueOf(subQueryAndBindings[0])); | ||
bindings.addAll(Arrays.asList(subQueryAndBindings).subList(1, subQueryAndBindings.length)); | ||
} | ||
|
||
String queryString = | ||
subQueries.size() == 1 | ||
? | ||
// If there's only one subQuery, just execute the one subQuery. | ||
subQueries.get(0) | ||
: | ||
// Construct "subQuery1 UNION subQuery2 UNION ... LIMIT N" | ||
TextUtils.join(" UNION ", subQueries) + " LIMIT " + target.getLimit(); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We need to make sure that we do not pass more than 999 bind elements. The best way to do this probably to validate that the query only contains 100 DNF terms (or what we decide the limit is). There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Can you also add parenthesis to indicate whether the LIMIT belongs to all statements or just the last one? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'll add an assertion here, and when I post the DNF calculation code I'll throw an exception if the number of DNF terms exceeds a certain number. Also updating this to |
||
|
||
SQLitePersistence.Query query = db.query(queryString).binding(bindings.toArray()); | ||
|
||
Set<DocumentKey> result = new HashSet<>(); | ||
query.forEach( | ||
|
@@ -408,8 +513,11 @@ public Set<DocumentKey> getDocumentsMatchingTarget(FieldIndex fieldIndex, Target | |
return result; | ||
} | ||
|
||
/** Returns a SQL query on 'index_entries' that unions all bounds. */ | ||
private SQLitePersistence.Query generateQuery( | ||
/** | ||
* Constructs a SQL query on 'index_entries' that unions all bounds. Returns an array with SQL | ||
* query string as the first element, followed by binding arguments. | ||
*/ | ||
private Object[] generateQueryAndBindings( | ||
Target target, | ||
int indexId, | ||
@Nullable List<Value> arrayValues, | ||
|
@@ -461,7 +569,7 @@ private SQLitePersistence.Query generateQuery( | |
// Fill in the bind ("question marks") variables. | ||
Object[] bindArgs = | ||
fillBounds(statementCount, indexId, arrayValues, lowerBounds, upperBounds, notIn); | ||
return db.query(sql.toString()).binding(bindArgs); | ||
return ObjectArrays.concat(sql.toString(), bindArgs); | ||
} | ||
|
||
/** Returns the bind arguments for all {@code statementCount} statements. */ | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
s/the/an
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done