-
Notifications
You must be signed in to change notification settings - Fork 626
WIP: OR Queries #3185
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
WIP: OR Queries #3185
Conversation
Add Filter.Java. Remove core/Filter.Java. Add CompositeFilter.java. Make the Filter class abstract. Purely formatting change. (ran googleJavaFormat). Added UnqualifiedFieldFilter, and `where` API. Update core/Query.java to be "aware" of composite filters. Fix the logic for `matches(doc)` for CompositeFilters. Fix remaining usages. Update RemoteSerializer.java. Add TODO for bundles. Add the other filter APIs. integrate with indexing. Respect the limit when evaluating multiple dnf terms. Remove UnqualifiedFieldFilter. Move validation to the right place. Don't impose limit on sub-filter queries. Enforce limit constraint for queries with more than 1 DNF term. Add the parsed filter to the query.
a780ffd
to
a31fa87
Compare
rebased. |
The public api surface has changed for the subproject firebase-firestore: Please update the api.txt files for the subprojects being affected by this change by running ./gradlew ${subproject}:generateApiTxtFile. Also perform a major/minor bump accordingly. |
Coverage ReportAffected SDKsNo changes between base commit (110b867) and head commit (888a2d5e). Test Logs
NotesHTML coverage reports can be produced locally with Head commit (888a2d5e) is created by Prow via merging commits: 110b867 a445976. |
Binary Size ReportAffected SDKs
Test Logs
NotesHead commit (888a2d5e) is created by Prow via merging commits: 110b867 a445976. |
The public api surface has changed for the subproject firebase-firestore: Please update the api.txt files for the subprojects being affected by this change by running ./gradlew ${subproject}:generateApiTxtFile. Also perform a major/minor bump accordingly. |
The public api surface has changed for the subproject firebase-firestore: Please update the api.txt files for the subprojects being affected by this change by running ./gradlew ${subproject}:generateApiTxtFile. Also perform a major/minor bump accordingly. |
The public api surface has changed for the subproject firebase-firestore: Please update the api.txt files for the subprojects being affected by this change by running ./gradlew ${subproject}:generateApiTxtFile. Also perform a major/minor bump accordingly. |
The public api surface has changed for the subproject firebase-firestore: Please update the api.txt files for the subprojects being affected by this change by running ./gradlew ${subproject}:generateApiTxtFile. Also perform a major/minor bump accordingly. |
1 similar comment
The public api surface has changed for the subproject firebase-firestore: Please update the api.txt files for the subprojects being affected by this change by running ./gradlew ${subproject}:generateApiTxtFile. Also perform a major/minor bump accordingly. |
|
posted a fix via #3213 |
} | ||
|
||
@Override | ||
public String getCanonicalId() { |
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.
Sebastian commented that I should updated this function to be consistent with existing canonical IDs. I haven't done that yet.
The public api surface has changed for the subproject firebase-firestore: Please update the api.txt files for the subprojects being affected by this change by running ./gradlew ${subproject}:generateApiTxtFile. Also perform a major/minor bump accordingly. |
@ehsannas: The following tests failed, say
Instructions for interacting with me using PR comments are available here. If you have questions or suggestions related to my behavior, please file an issue against the kubernetes/test-infra repository. I understand the commands that are listed here. |
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.
Some high level comments sprinkled with random nits. Overall, the functionality is in a good place. It probably makes sense to split this up into multiple PRs soon so that we can review smaller units in greater detail and get them merged.
} | ||
|
||
@NonNull | ||
public static Filter inList(@NonNull String field, @Nullable Object value) { |
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.
I left a comment regarding the naming of this method in the API proposal. This should probably be inArray
.
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.
Acknowledged
public Query where(@NonNull Filter... filters) { | ||
// If no filters were provided, we can return the current query. | ||
// TODO (ehsann): Maybe we shouldn't accept zero filters. | ||
if (filters.length == 0) { |
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.
Allowing zero filters makes it easier to programatically add filters.
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.
Acknowledged
} | ||
|
||
/** | ||
* Parses the given documentIdValue into a ReferenceValue, throwing appropriate errors if the | ||
* value is anything other than a DocumentReference or String, or if the string is malformed. | ||
*/ | ||
private Value parseDocumentIdValue(Object documentIdValue) { | ||
private Value parseDocumentIdValue( |
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.
This method should be static if you end up passing in all instance members. It however looks like you are just passing in this.firestore
and this.query
so I think you can probably revert this change altogether.
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.
yeah, this should be reverted. I moved this code to Filter and then moved it back here, and forgot to change the signature back to its original form.
* | ||
* @return A new field filter that contains the parsed Value. | ||
*/ | ||
private FieldFilter parseFieldFilterValue(FieldFilter filter) { |
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.
Can we refactor this so that FieldFilters always have Value
s? Maybe we can have com.google.firebase.fiterstore.Filter
's that use Objects
and FieldFilter
s that use Value.
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.
I'll try this suggestion.
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.
@schmidt-sebastian I think this wouldn't be a good idea because FieldFilter
and CompositeFilter
must inherit from Filter
. If we add field and operator and value (as Object
) in com.google.firebase.fiterstore.Filter
, then FieldFilter
and CompositeFilter
will have those members too.
In this PR:
Filter Class (no members)
^ ^
| |
| FieldFilter Class. Members:
| Field
| Op
| value (Value)
| valueObject (Object)
|
CompositeFilter Class. Members:
Filter[]
logicType
If we keep Object
in the API layer:
Filter Class. Members: Field, Op, valueObject (Object)
^ ^
| |
| FieldFilter Class. Members:
| Field(by inheritance)
| Op(by inheritance)
| valueObject (Object) (by inheritance)
| value (Value)
|
CompositeFilter Class. Members:
Field(by inheritance)
Op(by inheritance)
valueObject (Object) (by inheritance)
Filter[]
logicType
Another alternative is to create a third Filter class (E.g. com.google.firebase.fiterstore.core.Filter
)
Filter Class (no members)
^ ^ ^
| | |
| | FieldFilter Class. Members:
| | Field
| | Op
| | value (Value)
| |
| |
| CompositeFilter Class. Members:
| Filter[]
| logicType
|
com.google.firebase.fiterstore.core.Filter Class. Members:
Field
Op
value (Object)
This is what I did initially (and sadly chose UnqualifiedFilter
as the name instead of core.Filter
).
What do you think? Did I misunderstand your suggestion?
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.
I think the Web API has a good precedent for this:
The API layer only knows about the Object
: https://github.com/firebase/firebase-js-sdk/blob/master/packages/firestore/src/lite-api/query.ts#L132
This then gets translated to Value
when we construct the internal Filter.
There is some duplication here (since you need to keep the filter operator) but the two classes are distinct from one another.
My main goal is to get the Object
out of our core layer and into the API layer. If you add an hierarchy here that shares some members you might not achieve this goal.
@@ -652,7 +610,7 @@ private Query orderBy( | |||
"Invalid query. You must not call Query.endAt() or Query.endBefore() before " | |||
+ "calling Query.orderBy()."); | |||
} | |||
validateOrderByField(fieldPath); |
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.
Is this intentional?
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.
yes, this check is moved to core/Query.java. Generally there was a bunch of validation checks that really didn't belong to the API layer and should have been done in core/Query.java I believe. The addition of new filters made it more necessary to move them. I'll carve out the code moves as separate PRs that we can easily review.
if (filter instanceof FieldFilter) { | ||
FieldFilter fieldfilter = (FieldFilter) filter; | ||
if (fieldfilter.isInequality()) { | ||
return fieldfilter.getField(); | ||
} | ||
} else if (filter instanceof CompositeFilter) { | ||
CompositeFilter compositeFilter = (CompositeFilter) filter; | ||
FieldFilter found = compositeFilter.getInequalityFilter(); | ||
if (found != null) { | ||
return found.getField(); | ||
} |
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.
Can there be a method on Filter that we can call?
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.
Acknowledged.
compositeFilter.firstFieldFilterWhere(f -> operators.contains(f.getOperator())); | ||
if (fieldFilter != null) { | ||
return fieldFilter.getOperator(); | ||
} |
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.
Same comment as a above - would we nice if we did not have to differentiate the different filter types.
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.
Acknowledged.
// DNF (Disjunctive Normal Form) representation of this target's filters. | ||
// Each element in this list is an AND filter of FieldFilters. | ||
// The query result is the disjunction (OR) of each of these AND filters. | ||
private final List<CompositeFilter> dnf; |
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.
The Target is essentially the proto representation. We should not store the DNF here.
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.
I see. Thinking about this more, I think we should be able to push it to SQLiteIndexManager.java as that's the only place where it's really needed.
*/ | ||
@Nullable | ||
FieldIndex getFieldIndex(Target target); | ||
FieldIndex getFieldIndex(Target target, @Nullable CompositeFilter andFilter); |
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.
The reason this method exists is just so that the QueryEngine can quickly short-circuit Index-based execution if no index is defined. The callsite is:
FieldIndex i = getFIeldIndex();
if (i != null) {...}
It seems like this now adds some complication as we need to pass more state between getFieldIndex
and getDocumentsMatchingTarget
. We should probably revisit how this works to minimize these interactions (i.e. add a hasMatchingIndex()
method or make getDocumentsMatchingTarget()
nullable).
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.
Yes, you got it exactly right. I think I know how to simplify it now.
return result; | ||
} | ||
|
||
// If there's a limit constraint, we should perform all branches of the query (as done above), |
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.
The limit handling should be pushed into SQLite.
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.
Acknowledged.
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.
Thanks a lot for going through this PR. The PR is larger than I originally wanted it to be. For the most part I agree with your comments. I will start slicing this PR into smaller, more contained changes that can be reviewed/landed more easily. Coming soon to GitHub....
Thanks!
} | ||
|
||
@NonNull | ||
public static Filter inList(@NonNull String field, @Nullable Object value) { |
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.
Acknowledged
@@ -388,6 +389,26 @@ public Query whereNotIn(@NonNull FieldPath fieldPath, @NonNull List<? extends Ob | |||
return whereHelper(fieldPath, Operator.NOT_IN, values); |
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.
yeah. Acknowledged.
public Query where(@NonNull Filter... filters) { | ||
// If no filters were provided, we can return the current query. | ||
// TODO (ehsann): Maybe we shouldn't accept zero filters. | ||
if (filters.length == 0) { |
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.
Acknowledged
} | ||
|
||
/** | ||
* Parses the given documentIdValue into a ReferenceValue, throwing appropriate errors if the | ||
* value is anything other than a DocumentReference or String, or if the string is malformed. | ||
*/ | ||
private Value parseDocumentIdValue(Object documentIdValue) { | ||
private Value parseDocumentIdValue( |
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.
yeah, this should be reverted. I moved this code to Filter and then moved it back here, and forgot to change the signature back to its original form.
* | ||
* @return A new field filter that contains the parsed Value. | ||
*/ | ||
private FieldFilter parseFieldFilterValue(FieldFilter filter) { |
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.
I'll try this suggestion.
if (filter instanceof FieldFilter) { | ||
FieldFilter fieldfilter = (FieldFilter) filter; | ||
if (fieldfilter.isInequality()) { | ||
return fieldfilter.getField(); | ||
} | ||
} else if (filter instanceof CompositeFilter) { | ||
CompositeFilter compositeFilter = (CompositeFilter) filter; | ||
FieldFilter found = compositeFilter.getInequalityFilter(); | ||
if (found != null) { | ||
return found.getField(); | ||
} |
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.
Acknowledged.
compositeFilter.firstFieldFilterWhere(f -> operators.contains(f.getOperator())); | ||
if (fieldFilter != null) { | ||
return fieldFilter.getOperator(); | ||
} |
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.
Acknowledged.
// DNF (Disjunctive Normal Form) representation of this target's filters. | ||
// Each element in this list is an AND filter of FieldFilters. | ||
// The query result is the disjunction (OR) of each of these AND filters. | ||
private final List<CompositeFilter> dnf; |
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.
I see. Thinking about this more, I think we should be able to push it to SQLiteIndexManager.java as that's the only place where it's really needed.
*/ | ||
@Nullable | ||
FieldIndex getFieldIndex(Target target); | ||
FieldIndex getFieldIndex(Target target, @Nullable CompositeFilter andFilter); |
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.
Yes, you got it exactly right. I think I know how to simplify it now.
return result; | ||
} | ||
|
||
// If there's a limit constraint, we should perform all branches of the query (as done above), |
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.
Acknowledged.
No description provided.