Skip to content

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

Closed
wants to merge 12 commits into from
Closed

WIP: OR Queries #3185

wants to merge 12 commits into from

Conversation

ehsannas
Copy link
Contributor

No description provided.

@google-cla google-cla bot added the cla: yes Override cla label Nov 29, 2021
@ehsannas ehsannas marked this pull request as draft November 29, 2021 17:23
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.
@ehsannas
Copy link
Contributor Author

rebased.

@google-oss-bot
Copy link
Contributor

The public api surface has changed for the subproject firebase-firestore:
error: Added method com.google.firebase.firestore.DocumentReference.getKey() [AddedMethod]
error: Added class com.google.firebase.firestore.Filter [AddedClass]
error: Added method com.google.firebase.firestore.FirebaseFirestore.getDatabaseId() [AddedMethod]
error: Added method com.google.firebase.firestore.FirebaseFirestore.getUserDataReader() [AddedMethod]
error: Added method com.google.firebase.firestore.Query.where(com.google.firebase.firestore.Filter...) [AddedMethod]

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.

@google-oss-bot
Copy link
Contributor

google-oss-bot commented Nov 29, 2021

Coverage Report

Affected SDKs

No changes between base commit (110b867) and head commit (888a2d5e).

Test Logs

Notes

HTML coverage reports can be produced locally with ./gradlew <product>:checkCoverage.
Report files are located at <product-build-dir>/reports/jacoco/.

Head commit (888a2d5e) is created by Prow via merging commits: 110b867 a445976.

@google-oss-bot
Copy link
Contributor

google-oss-bot commented Nov 29, 2021

Binary Size Report

Affected SDKs

  • firebase-firestore

    Type Base (110b867) Head (888a2d5e) Diff
    aar 1.23 MB 1.24 MB +9.83 kB (+0.8%)
    apk (release) 3.32 MB 3.33 MB +4.12 kB (+0.1%)

Test Logs

Notes

Head commit (888a2d5e) is created by Prow via merging commits: 110b867 a445976.

@google-oss-bot
Copy link
Contributor

The public api surface has changed for the subproject firebase-firestore:
error: Added class com.google.firebase.firestore.Filter [AddedClass]
error: Added method com.google.firebase.firestore.Query.where(com.google.firebase.firestore.Filter...) [AddedMethod]

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.

@google-oss-bot
Copy link
Contributor

The public api surface has changed for the subproject firebase-firestore:
error: Added class com.google.firebase.firestore.Filter [AddedClass]
error: Added method com.google.firebase.firestore.Query.where(com.google.firebase.firestore.Filter...) [AddedMethod]

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.

@google-oss-bot
Copy link
Contributor

The public api surface has changed for the subproject firebase-firestore:
warning: Added method com.google.firebase.firestore.Filter.equals(Object) [AddedAbstractMethod]

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.

@google-oss-bot
Copy link
Contributor

The public api surface has changed for the subproject firebase-firestore:
warning: Added method com.google.firebase.firestore.Filter.equals(Object) [AddedAbstractMethod]
warning: Added method com.google.firebase.firestore.Filter.hashCode() [AddedAbstractMethod]

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
@google-oss-bot
Copy link
Contributor

The public api surface has changed for the subproject firebase-firestore:
warning: Added method com.google.firebase.firestore.Filter.equals(Object) [AddedAbstractMethod]
warning: Added method com.google.firebase.firestore.Filter.hashCode() [AddedAbstractMethod]

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
Copy link
Contributor Author

ehsannas commented Dec 7, 2021

Task :firebase-app-distribution:apiInformation FAILED... Looks like tot is broken

@ehsannas
Copy link
Contributor Author

ehsannas commented Dec 7, 2021

posted a fix via #3213

}

@Override
public String getCanonicalId() {
Copy link
Contributor Author

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.

@google-oss-bot
Copy link
Contributor

The public api surface has changed for the subproject firebase-firestore:
warning: Added method com.google.firebase.firestore.Filter.equals(Object) [AddedAbstractMethod]
warning: Added method com.google.firebase.firestore.Filter.hashCode() [AddedAbstractMethod]

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.

@google-oss-bot
Copy link
Contributor

@ehsannas: The following tests failed, say /retest to rerun them all:

Test name Commit Details Rerun command
api-information a445976 link /test api-information
check-changed a445976 link /test check-changed
device-check-changed a445976 link /test device-check-changed

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.

Copy link
Contributor

@schmidt-sebastian schmidt-sebastian left a 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) {
Copy link
Contributor

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.

Copy link
Contributor Author

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) {
Copy link
Contributor

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.

Copy link
Contributor Author

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(
Copy link
Contributor

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.

Copy link
Contributor Author

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) {
Copy link
Contributor

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 Values? Maybe we can have com.google.firebase.fiterstore.Filter's that use Objects and FieldFilters that use Value.

Copy link
Contributor Author

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.

Copy link
Contributor Author

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?

Copy link
Contributor

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);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this intentional?

Copy link
Contributor Author

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.

Comment on lines 215 to +225
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();
}
Copy link
Contributor

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?

Copy link
Contributor Author

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();
}
Copy link
Contributor

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.

Copy link
Contributor Author

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;
Copy link
Contributor

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.

Copy link
Contributor Author

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);
Copy link
Contributor

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).

Copy link
Contributor Author

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),
Copy link
Contributor

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.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Acknowledged.

Copy link
Contributor Author

@ehsannas ehsannas left a 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) {
Copy link
Contributor Author

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);
Copy link
Contributor Author

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) {
Copy link
Contributor Author

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(
Copy link
Contributor Author

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) {
Copy link
Contributor Author

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.

Comment on lines 215 to +225
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();
}
Copy link
Contributor Author

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();
}
Copy link
Contributor Author

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;
Copy link
Contributor Author

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);
Copy link
Contributor Author

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),
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Acknowledged.

@ehsannas ehsannas closed this Aug 2, 2022
@firebase firebase locked and limited conversation to collaborators Sep 2, 2022
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants