From 55e5e4788a0d0e343f7029ba752ed776695fd436 Mon Sep 17 00:00:00 2001 From: Ehsan Nasiri Date: Wed, 22 Jun 2022 17:43:49 -0500 Subject: [PATCH 01/13] Support for Composite Filters. --- .../com/google/cloud/firestore/Filter.java | 182 ++++++++ .../com/google/cloud/firestore/Query.java | 410 +++++++++++------- .../com/google/cloud/firestore/QueryTest.java | 18 +- 3 files changed, 438 insertions(+), 172 deletions(-) create mode 100644 google-cloud-firestore/src/main/java/com/google/cloud/firestore/Filter.java diff --git a/google-cloud-firestore/src/main/java/com/google/cloud/firestore/Filter.java b/google-cloud-firestore/src/main/java/com/google/cloud/firestore/Filter.java new file mode 100644 index 000000000..340667597 --- /dev/null +++ b/google-cloud-firestore/src/main/java/com/google/cloud/firestore/Filter.java @@ -0,0 +1,182 @@ +// Copyright 2021 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package com.google.cloud.firestore; + +import com.google.firestore.v1.StructuredQuery; +import com.google.firestore.v1.StructuredQuery.FieldFilter.Operator; +import java.util.Arrays; +import java.util.List; +import javax.annotation.Nullable; +import org.checkerframework.checker.nullness.qual.NonNull; + +/** @hide */ +public class Filter { + static class UnaryFilter extends Filter { + private final FieldPath field; + private final Operator operator; + private final Object value; + + public UnaryFilter(FieldPath field, Operator operator, @Nullable Object value) { + this.field = field; + this.operator = operator; + this.value = value; + } + + public FieldPath getField() { + return field; + } + + public Operator getOperator() { + return operator; + } + + @Nullable + public Object getValue() { + return value; + } + } + + static class CompositeFilter extends Filter { + private final List filters; + private final StructuredQuery.CompositeFilter.Operator operator; + + public CompositeFilter( + @NonNull List filters, StructuredQuery.CompositeFilter.Operator operator) { + this.filters = filters; + this.operator = operator; + } + + public List getFilters() { + return filters; + } + + public StructuredQuery.CompositeFilter.Operator getOperator() { + return operator; + } + } + + @NonNull + public static Filter equalTo(@NonNull String field, @Nullable Object value) { + return equalTo(FieldPath.fromDotSeparatedString(field), value); + } + + @NonNull + public static Filter equalTo(@NonNull FieldPath fieldPath, @Nullable Object value) { + return new UnaryFilter(fieldPath, Operator.EQUAL, value); + } + + @NonNull + public static Filter notEqualTo(@NonNull String field, @Nullable Object value) { + return notEqualTo(FieldPath.fromDotSeparatedString(field), value); + } + + @NonNull + public static Filter notEqualTo(@NonNull FieldPath fieldPath, @Nullable Object value) { + return new UnaryFilter(fieldPath, Operator.NOT_EQUAL, value); + } + + @NonNull + public static Filter greaterThan(@NonNull String field, @Nullable Object value) { + return greaterThan(FieldPath.fromDotSeparatedString(field), value); + } + + @NonNull + public static Filter greaterThan(@NonNull FieldPath fieldPath, @Nullable Object value) { + return new UnaryFilter(fieldPath, Operator.GREATER_THAN, value); + } + + @NonNull + public static Filter greaterThanOrEqualTo(@NonNull String field, @Nullable Object value) { + return greaterThanOrEqualTo(FieldPath.fromDotSeparatedString(field), value); + } + + @NonNull + public static Filter greaterThanOrEqualTo(@NonNull FieldPath fieldPath, @Nullable Object value) { + return new UnaryFilter(fieldPath, Operator.GREATER_THAN_OR_EQUAL, value); + } + + @NonNull + public static Filter lessThan(@NonNull String field, @Nullable Object value) { + return lessThan(FieldPath.fromDotSeparatedString(field), value); + } + + @NonNull + public static Filter lessThan(@NonNull FieldPath fieldPath, @Nullable Object value) { + return new UnaryFilter(fieldPath, Operator.LESS_THAN, value); + } + + @NonNull + public static Filter lessThanOrEqualTo(@NonNull String field, @Nullable Object value) { + return lessThanOrEqualTo(FieldPath.fromDotSeparatedString(field), value); + } + + @NonNull + public static Filter lessThanOrEqualTo(@NonNull FieldPath fieldPath, @Nullable Object value) { + return new UnaryFilter(fieldPath, Operator.LESS_THAN_OR_EQUAL, value); + } + + @NonNull + public static Filter arrayContains(@NonNull String field, @Nullable Object value) { + return arrayContains(FieldPath.fromDotSeparatedString(field), value); + } + + @NonNull + public static Filter arrayContains(@NonNull FieldPath fieldPath, @Nullable Object value) { + return new UnaryFilter(fieldPath, Operator.ARRAY_CONTAINS, value); + } + + @NonNull + public static Filter arrayContainsAny(@NonNull String field, @Nullable Object value) { + return arrayContainsAny(FieldPath.fromDotSeparatedString(field), value); + } + + @NonNull + public static Filter arrayContainsAny(@NonNull FieldPath fieldPath, @Nullable Object value) { + return new UnaryFilter(fieldPath, Operator.ARRAY_CONTAINS_ANY, value); + } + + @NonNull + public static Filter inArray(@NonNull String field, @Nullable Object value) { + return inArray(FieldPath.fromDotSeparatedString(field), value); + } + + @NonNull + public static Filter inArray(@NonNull FieldPath fieldPath, @Nullable Object value) { + return new UnaryFilter(fieldPath, Operator.IN, value); + } + + @NonNull + public static Filter notInArray(@NonNull String field, @Nullable Object value) { + return notInArray(FieldPath.fromDotSeparatedString(field), value); + } + + @NonNull + public static Filter notInArray(@NonNull FieldPath fieldPath, @Nullable Object value) { + return new UnaryFilter(fieldPath, Operator.NOT_IN, value); + } + + @NonNull + public static Filter or(Filter... filters) { + // TODO(orquery): Change this to Operator.OR once it is available. + return new CompositeFilter( + Arrays.asList(filters), StructuredQuery.CompositeFilter.Operator.OPERATOR_UNSPECIFIED); + } + + @NonNull + public static Filter and(Filter... filters) { + return new CompositeFilter( + Arrays.asList(filters), StructuredQuery.CompositeFilter.Operator.AND); + } +} diff --git a/google-cloud-firestore/src/main/java/com/google/cloud/firestore/Query.java b/google-cloud-firestore/src/main/java/com/google/cloud/firestore/Query.java index fd6a7a101..7238b7df8 100644 --- a/google-cloud-firestore/src/main/java/com/google/cloud/firestore/Query.java +++ b/google-cloud-firestore/src/main/java/com/google/cloud/firestore/Query.java @@ -37,6 +37,7 @@ import com.google.api.gax.rpc.StreamController; import com.google.auto.value.AutoValue; import com.google.cloud.Timestamp; +import com.google.cloud.firestore.Filter.UnaryFilter; import com.google.cloud.firestore.Query.QueryOptions.Builder; import com.google.cloud.firestore.v1.FirestoreSettings; import com.google.common.base.Preconditions; @@ -50,6 +51,7 @@ import com.google.firestore.v1.StructuredQuery; import com.google.firestore.v1.StructuredQuery.CollectionSelector; import com.google.firestore.v1.StructuredQuery.CompositeFilter; +import com.google.firestore.v1.StructuredQuery.FieldFilter.Operator; import com.google.firestore.v1.StructuredQuery.FieldReference; import com.google.firestore.v1.StructuredQuery.Filter; import com.google.firestore.v1.StructuredQuery.Order; @@ -60,6 +62,7 @@ import io.opencensus.trace.AttributeValue; import io.opencensus.trace.Tracing; import java.util.ArrayList; +import java.util.Collections; import java.util.Comparator; import java.util.Iterator; import java.util.List; @@ -97,31 +100,134 @@ StructuredQuery.Direction getDirection() { } } - abstract static class FieldFilter { - protected final FieldReference fieldReference; + abstract static class FilterInternal { + /** Returns a list of all field filters that are contained within this filter */ + public abstract List getFlattenedFilters(); - FieldFilter(FieldReference fieldReference) { - this.fieldReference = fieldReference; - } + /** Returns a list of all filters that are contained within this filter */ + public abstract List getFilters(); - static FieldFilter fromProto(StructuredQuery.Filter filter) { - Preconditions.checkArgument( - !filter.hasCompositeFilter(), "Cannot deserialize nested composite filters"); + /** Returns the field of the first filter that's an inequality, or null if none. */ + @Nullable + public abstract FieldReference getFirstInequalityField(); + + /** Returns the proto representation of this filter */ + abstract Filter toProto(); + + static FilterInternal fromProto(StructuredQuery.Filter filter) { + if (filter.hasUnaryFilter()) { + return new Query.UnaryFilter( + filter.getUnaryFilter().getField(), filter.getUnaryFilter().getOp()); + } if (filter.hasFieldFilter()) { return new ComparisonFilter( filter.getFieldFilter().getField(), filter.getFieldFilter().getOp(), filter.getFieldFilter().getValue()); - } else { - Preconditions.checkState(filter.hasUnaryFilter(), "Expected unary of field filter"); - return new UnaryFilter(filter.getUnaryFilter().getField(), filter.getUnaryFilter().getOp()); } + + // `filter` must be a composite filter. + Preconditions.checkArgument(filter.hasCompositeFilter(), "Unknown filter type."); + CompositeFilter compositeFilter = filter.getCompositeFilter(); + // A composite filter with only 1 sub-filter should be reduced to its sub-filter. + if (compositeFilter.getFiltersCount() == 1) { + return FilterInternal.fromProto(compositeFilter.getFiltersList().get(0)); + } + List filters = new ArrayList<>(); + for (StructuredQuery.Filter subfilter : compositeFilter.getFiltersList()) { + filters.add(FilterInternal.fromProto(subfilter)); + } + return new CompositeComparisonFilter(filters, compositeFilter.getOp()); + } + } + + static class CompositeComparisonFilter extends FilterInternal { + private final List filters; + private final StructuredQuery.CompositeFilter.Operator operator; + + // Memoized list of all field filters that can be found by traversing the tree of filters + // contained in this composite filter. + private List memoizedFlattenedFilters; + + public CompositeComparisonFilter( + List filters, StructuredQuery.CompositeFilter.Operator operator) { + this.filters = filters; + this.operator = operator; + } + + @Override + public List getFilters() { + return filters; + } + + @Nullable + @Override + public FieldReference getFirstInequalityField() { + for (FieldFilter fieldFilter : getFlattenedFilters()) { + if (fieldFilter.isInequalityFilter()) { + return fieldFilter.fieldReference; + } + } + return null; + } + + public StructuredQuery.CompositeFilter.Operator getOperator() { + return operator; + } + + public boolean isConjunction() { + return operator == CompositeFilter.Operator.AND; + } + + @Override + public List getFlattenedFilters() { + if (memoizedFlattenedFilters != null) { + return memoizedFlattenedFilters; + } + memoizedFlattenedFilters = new ArrayList<>(); + for (FilterInternal subfilter : filters) { + memoizedFlattenedFilters.addAll(subfilter.getFlattenedFilters()); + } + return memoizedFlattenedFilters; + } + + @Override + Filter toProto() { + // A composite filter that only contains one sub-filter is equivalent to the sub-filter. + if (filters.size() == 1) { + return filters.get(0).toProto(); + } + + Filter.Builder protoFilter = Filter.newBuilder(); + StructuredQuery.CompositeFilter.Builder compositeFilter = + StructuredQuery.CompositeFilter.newBuilder(); + compositeFilter.setOp(operator); + for (FilterInternal filter : filters) { + compositeFilter.addFilters(filter.toProto()); + } + protoFilter.setCompositeFilter(compositeFilter.build()); + return protoFilter.build(); + } + } + + abstract static class FieldFilter extends FilterInternal { + protected final FieldReference fieldReference; + + FieldFilter(FieldReference fieldReference) { + this.fieldReference = fieldReference; } abstract boolean isInequalityFilter(); - abstract Filter toProto(); + public List getFilters() { + return Collections.singletonList(this); + } + + @Override + public List getFlattenedFilters() { + return Collections.singletonList(this); + } } private static class UnaryFilter extends FieldFilter { @@ -138,6 +244,12 @@ boolean isInequalityFilter() { return false; } + @Nullable + @Override + public FieldReference getFirstInequalityField() { + return null; + } + Filter toProto() { Filter.Builder result = Filter.newBuilder(); result.getUnaryFilterBuilder().setField(fieldReference).setOp(operator); @@ -169,6 +281,7 @@ static class ComparisonFilter extends FieldFilter { this.operator = operator; } + // TODO(ehsan): This seems out of date. @Override boolean isInequalityFilter() { return operator.equals(GREATER_THAN) @@ -177,6 +290,15 @@ boolean isInequalityFilter() { || operator.equals(LESS_THAN_OR_EQUAL); } + @Nullable + @Override + public FieldReference getFirstInequalityField() { + if (isInequalityFilter()) { + return fieldReference; + } + return null; + } + Filter toProto() { Filter.Builder result = Filter.newBuilder(); result.getFieldFilterBuilder().setField(fieldReference).setValue(value).setOp(operator); @@ -252,7 +374,9 @@ abstract static class QueryOptions { abstract @Nullable Cursor getEndCursor(); - abstract ImmutableList getFieldFilters(); + // abstract ImmutableList getFieldFilters(); + + abstract ImmutableList getFilters(); abstract ImmutableList getFieldOrders(); @@ -272,7 +396,8 @@ static Builder builder() { .setAllDescendants(false) .setLimitType(LimitType.First) .setFieldOrders(ImmutableList.of()) - .setFieldFilters(ImmutableList.of()) + // .setFieldFilters(ImmutableList.of()) + .setFilters(ImmutableList.of()) .setFieldProjections(ImmutableList.of()) .setKindless(false) .setRequireConsistency(true); @@ -298,7 +423,9 @@ abstract static class Builder { abstract Builder setEndCursor(@Nullable Cursor value); - abstract Builder setFieldFilters(ImmutableList value); + // abstract Builder setFieldFilters(ImmutableList value); + + abstract Builder setFilters(ImmutableList value); abstract Builder setFieldOrders(ImmutableList value); @@ -348,9 +475,10 @@ private ImmutableList createImplicitOrderBy() { // If no explicit ordering is specified, use the first inequality to define an implicit order. if (implicitOrders.isEmpty()) { - for (FieldFilter fieldFilter : options.getFieldFilters()) { - if (fieldFilter.isInequalityFilter()) { - implicitOrders.add(new FieldOrder(fieldFilter.fieldReference, Direction.ASCENDING)); + for (FilterInternal filter : options.getFilters()) { + FieldReference fieldReference = filter.getFirstInequalityField(); + if (fieldReference != null) { + implicitOrders.add(new FieldOrder(fieldReference, Direction.ASCENDING)); break; } } @@ -499,23 +627,7 @@ public Query whereEqualTo(@Nonnull String field, @Nullable Object value) { */ @Nonnull public Query whereEqualTo(@Nonnull FieldPath fieldPath, @Nullable Object value) { - Preconditions.checkState( - options.getStartCursor() == null && options.getEndCursor() == null, - "Cannot call whereEqualTo() after defining a boundary with startAt(), " - + "startAfter(), endBefore() or endAt()."); - - if (isUnaryComparison(value)) { - Builder newOptions = options.toBuilder(); - StructuredQuery.UnaryFilter.Operator op = - value == null - ? StructuredQuery.UnaryFilter.Operator.IS_NULL - : StructuredQuery.UnaryFilter.Operator.IS_NAN; - UnaryFilter newFieldFilter = new UnaryFilter(fieldPath.toProto(), op); - newOptions.setFieldFilters(append(options.getFieldFilters(), newFieldFilter)); - return new Query(rpcContext, newOptions.build()); - } else { - return whereHelper(fieldPath, EQUAL, value); - } + return where(new com.google.cloud.firestore.Filter.UnaryFilter(fieldPath, EQUAL, value)); } /** @@ -541,23 +653,7 @@ public Query whereNotEqualTo(@Nonnull String field, @Nullable Object value) { */ @Nonnull public Query whereNotEqualTo(@Nonnull FieldPath fieldPath, @Nullable Object value) { - Preconditions.checkState( - options.getStartCursor() == null && options.getEndCursor() == null, - "Cannot call whereNotEqualTo() after defining a boundary with startAt(), " - + "startAfter(), endBefore() or endAt()."); - - if (isUnaryComparison(value)) { - Builder newOptions = options.toBuilder(); - StructuredQuery.UnaryFilter.Operator op = - value == null - ? StructuredQuery.UnaryFilter.Operator.IS_NOT_NULL - : StructuredQuery.UnaryFilter.Operator.IS_NOT_NAN; - UnaryFilter newFieldFilter = new UnaryFilter(fieldPath.toProto(), op); - newOptions.setFieldFilters(append(options.getFieldFilters(), newFieldFilter)); - return new Query(rpcContext, newOptions.build()); - } else { - return whereHelper(fieldPath, NOT_EQUAL, value); - } + return where(new com.google.cloud.firestore.Filter.UnaryFilter(fieldPath, NOT_EQUAL, value)); } /** @@ -583,11 +679,7 @@ public Query whereLessThan(@Nonnull String field, @Nonnull Object value) { */ @Nonnull public Query whereLessThan(@Nonnull FieldPath fieldPath, @Nonnull Object value) { - Preconditions.checkState( - options.getStartCursor() == null && options.getEndCursor() == null, - "Cannot call whereLessThan() after defining a boundary with startAt(), " - + "startAfter(), endBefore() or endAt()."); - return whereHelper(fieldPath, LESS_THAN, value); + return where(new com.google.cloud.firestore.Filter.UnaryFilter(fieldPath, LESS_THAN, value)); } /** @@ -613,11 +705,8 @@ public Query whereLessThanOrEqualTo(@Nonnull String field, @Nonnull Object value */ @Nonnull public Query whereLessThanOrEqualTo(@Nonnull FieldPath fieldPath, @Nonnull Object value) { - Preconditions.checkState( - options.getStartCursor() == null && options.getEndCursor() == null, - "Cannot call whereLessThanOrEqualTo() after defining a boundary with startAt(), " - + "startAfter(), endBefore() or endAt()."); - return whereHelper(fieldPath, LESS_THAN_OR_EQUAL, value); + return where( + new com.google.cloud.firestore.Filter.UnaryFilter(fieldPath, LESS_THAN_OR_EQUAL, value)); } /** @@ -643,11 +732,7 @@ public Query whereGreaterThan(@Nonnull String field, @Nonnull Object value) { */ @Nonnull public Query whereGreaterThan(@Nonnull FieldPath fieldPath, @Nonnull Object value) { - Preconditions.checkState( - options.getStartCursor() == null && options.getEndCursor() == null, - "Cannot call whereGreaterThan() after defining a boundary with startAt(), " - + "startAfter(), endBefore() or endAt()."); - return whereHelper(fieldPath, GREATER_THAN, value); + return where(new com.google.cloud.firestore.Filter.UnaryFilter(fieldPath, GREATER_THAN, value)); } /** @@ -673,11 +758,8 @@ public Query whereGreaterThanOrEqualTo(@Nonnull String field, @Nonnull Object va */ @Nonnull public Query whereGreaterThanOrEqualTo(@Nonnull FieldPath fieldPath, @Nonnull Object value) { - Preconditions.checkState( - options.getStartCursor() == null && options.getEndCursor() == null, - "Cannot call whereGreaterThanOrEqualTo() after defining a boundary with startAt(), " - + "startAfter(), endBefore() or endAt()."); - return whereHelper(fieldPath, GREATER_THAN_OR_EQUAL, value); + return where( + new com.google.cloud.firestore.Filter.UnaryFilter(fieldPath, GREATER_THAN_OR_EQUAL, value)); } /** @@ -711,11 +793,8 @@ public Query whereArrayContains(@Nonnull String field, @Nonnull Object value) { */ @Nonnull public Query whereArrayContains(@Nonnull FieldPath fieldPath, @Nonnull Object value) { - Preconditions.checkState( - options.getStartCursor() == null && options.getEndCursor() == null, - "Cannot call whereArrayContains() after defining a boundary with startAt(), " - + "startAfter(), endBefore() or endAt()."); - return whereHelper(fieldPath, ARRAY_CONTAINS, value); + return where( + new com.google.cloud.firestore.Filter.UnaryFilter(fieldPath, ARRAY_CONTAINS, value)); } /** @@ -733,11 +812,7 @@ public Query whereArrayContains(@Nonnull FieldPath fieldPath, @Nonnull Object va @Nonnull public Query whereArrayContainsAny( @Nonnull String field, @Nonnull List values) { - Preconditions.checkState( - options.getStartCursor() == null && options.getEndCursor() == null, - "Cannot call whereArrayContainsAny() after defining a boundary with startAt(), " - + "startAfter(), endBefore() or endAt()."); - return whereHelper(FieldPath.fromDotSeparatedString(field), ARRAY_CONTAINS_ANY, values); + return whereArrayContainsAny(FieldPath.fromDotSeparatedString(field), values); } /** @@ -755,11 +830,8 @@ public Query whereArrayContainsAny( @Nonnull public Query whereArrayContainsAny( @Nonnull FieldPath fieldPath, @Nonnull List values) { - Preconditions.checkState( - options.getStartCursor() == null && options.getEndCursor() == null, - "Cannot call whereArrayContainsAny() after defining a boundary with startAt(), " - + "startAfter(), endBefore() or endAt()."); - return whereHelper(fieldPath, ARRAY_CONTAINS_ANY, values); + return where( + new com.google.cloud.firestore.Filter.UnaryFilter(fieldPath, ARRAY_CONTAINS_ANY, values)); } /** @@ -775,11 +847,7 @@ public Query whereArrayContainsAny( */ @Nonnull public Query whereIn(@Nonnull String field, @Nonnull List values) { - Preconditions.checkState( - options.getStartCursor() == null && options.getEndCursor() == null, - "Cannot call whereIn() after defining a boundary with startAt(), " - + "startAfter(), endBefore() or endAt()."); - return whereHelper(FieldPath.fromDotSeparatedString(field), IN, values); + return whereIn(FieldPath.fromDotSeparatedString(field), values); } /** @@ -795,11 +863,7 @@ public Query whereIn(@Nonnull String field, @Nonnull List valu */ @Nonnull public Query whereIn(@Nonnull FieldPath fieldPath, @Nonnull List values) { - Preconditions.checkState( - options.getStartCursor() == null && options.getEndCursor() == null, - "Cannot call whereIn() after defining a boundary with startAt(), " - + "startAfter(), endBefore() or endAt()."); - return whereHelper(fieldPath, IN, values); + return where(new com.google.cloud.firestore.Filter.UnaryFilter(fieldPath, IN, values)); } /** @@ -815,11 +879,7 @@ public Query whereIn(@Nonnull FieldPath fieldPath, @Nonnull List values) { - Preconditions.checkState( - options.getStartCursor() == null && options.getEndCursor() == null, - "Cannot call whereNotIn() after defining a boundary with startAt(), " - + "startAfter(), endBefore() or endAt()."); - return whereHelper(FieldPath.fromDotSeparatedString(field), NOT_IN, values); + return whereNotIn(FieldPath.fromDotSeparatedString(field), values); } /** @@ -835,49 +895,92 @@ public Query whereNotIn(@Nonnull String field, @Nonnull List v */ @Nonnull public Query whereNotIn(@Nonnull FieldPath fieldPath, @Nonnull List values) { + return where(new com.google.cloud.firestore.Filter.UnaryFilter(fieldPath, NOT_IN, values)); + } + + // TODO(orquery): This method will become public API. Change visibility and add documentation. + Query where(com.google.cloud.firestore.Filter filter) { Preconditions.checkState( options.getStartCursor() == null && options.getEndCursor() == null, - "Cannot call whereNotIn() after defining a boundary with startAt(), " + "Cannot call a where() clause after defining a boundary with startAt(), " + "startAfter(), endBefore() or endAt()."); - return whereHelper(fieldPath, NOT_IN, values); + FilterInternal parsedFilter = parseFilter(filter); + if (parsedFilter.getFilters().isEmpty()) { + // Return the existing query if not adding any more filters (e.g. an empty composite filter). + return this; + } + Builder newOptions = options.toBuilder(); + newOptions.setFilters(append(options.getFilters(), parsedFilter)); + return new Query(rpcContext, newOptions.build()); } - private Query whereHelper( - FieldPath fieldPath, StructuredQuery.FieldFilter.Operator operator, Object value) { - Preconditions.checkArgument( - !isUnaryComparison(value), - "Cannot use '%s' in field comparison. Use an equality filter instead.", - value); - - if (fieldPath.equals(FieldPath.DOCUMENT_ID)) { - if (operator == ARRAY_CONTAINS || operator == ARRAY_CONTAINS_ANY) { - throw new IllegalArgumentException( - String.format( - "Invalid query. You cannot perform '%s' queries on FieldPath.documentId().", - operator.toString())); - } else if (operator == IN | operator == NOT_IN) { - if (!(value instanceof List) || ((List) value).isEmpty()) { + FilterInternal parseFilter(com.google.cloud.firestore.Filter filter) { + if (filter instanceof com.google.cloud.firestore.Filter.UnaryFilter) { + return parseFieldFilter((com.google.cloud.firestore.Filter.UnaryFilter) filter); + } + return parseCompositeFilter((com.google.cloud.firestore.Filter.CompositeFilter) filter); + } + + private FieldFilter parseFieldFilter( + com.google.cloud.firestore.Filter.UnaryFilter fieldFilterData) { + Object value = fieldFilterData.getValue(); + Operator operator = fieldFilterData.getOperator(); + FieldPath fieldPath = fieldFilterData.getField(); + + if ((operator == EQUAL || operator == NOT_EQUAL) && isUnaryComparison(value)) { + StructuredQuery.UnaryFilter.Operator unaryOp = + operator == EQUAL + ? (value == null + ? StructuredQuery.UnaryFilter.Operator.IS_NULL + : StructuredQuery.UnaryFilter.Operator.IS_NAN) + : (value == null + ? StructuredQuery.UnaryFilter.Operator.IS_NOT_NULL + : StructuredQuery.UnaryFilter.Operator.IS_NOT_NAN); + return new UnaryFilter(fieldPath.toProto(), unaryOp); + } else { + if (fieldPath.equals(FieldPath.DOCUMENT_ID)) { + if (operator == ARRAY_CONTAINS || operator == ARRAY_CONTAINS_ANY) { throw new IllegalArgumentException( String.format( - "Invalid Query. A non-empty array is required for '%s' filters.", + "Invalid query. You cannot perform '%s' queries on FieldPath.documentId().", operator.toString())); + } else if (operator == IN | operator == NOT_IN) { + if (!(value instanceof List) || ((List) value).isEmpty()) { + throw new IllegalArgumentException( + String.format( + "Invalid Query. A non-empty array is required for '%s' filters.", + operator.toString())); + } + List referenceList = new ArrayList<>(); + for (Object arrayValue : (List) value) { + Object convertedValue = this.convertReference(arrayValue); + referenceList.add(convertedValue); + } + value = referenceList; + } else { + value = this.convertReference(value); } - List referenceList = new ArrayList<>(); - for (Object arrayValue : (List) value) { - Object convertedValue = this.convertReference(arrayValue); - referenceList.add(convertedValue); - } - value = referenceList; - } else { - value = this.convertReference(value); } + return new ComparisonFilter(fieldPath.toProto(), operator, encodeValue(fieldPath, value)); } + } - Builder newOptions = options.toBuilder(); - ComparisonFilter newFieldFilter = - new ComparisonFilter(fieldPath.toProto(), operator, encodeValue(fieldPath, value)); - newOptions.setFieldFilters(append(options.getFieldFilters(), newFieldFilter)); - return new Query(rpcContext, newOptions.build()); + private FilterInternal parseCompositeFilter( + com.google.cloud.firestore.Filter.CompositeFilter compositeFilterData) { + List parsedFilters = new ArrayList<>(); + for (com.google.cloud.firestore.Filter filter : compositeFilterData.getFilters()) { + FilterInternal parsedFilter = parseFilter(filter); + if (!parsedFilter.getFilters().isEmpty()) { + parsedFilters.add(parsedFilter); + } + } + + // For composite filters containing 1 filter, return the only filter. + // For example: AND(FieldFilter1) == FieldFilter1 + if (parsedFilters.size() == 1) { + return parsedFilters.get(0); + } + return new CompositeComparisonFilter(parsedFilters, compositeFilterData.getOperator()); } /** @@ -1264,25 +1367,11 @@ private StructuredQuery.Builder buildWithoutClientTranslation() { collectionSelector.setAllDescendants(options.getAllDescendants()); structuredQuery.addFrom(collectionSelector); - if (options.getFieldFilters().size() == 1) { - Filter filter = options.getFieldFilters().get(0).toProto(); - if (filter.hasFieldFilter()) { - structuredQuery.getWhereBuilder().setFieldFilter(filter.getFieldFilter()); - } else { - Preconditions.checkState( - filter.hasUnaryFilter(), "Expected a UnaryFilter or a FieldFilter."); - structuredQuery.getWhereBuilder().setUnaryFilter(filter.getUnaryFilter()); - } - } else if (options.getFieldFilters().size() > 1) { - Filter.Builder filter = Filter.newBuilder(); - StructuredQuery.CompositeFilter.Builder compositeFilter = - StructuredQuery.CompositeFilter.newBuilder(); - compositeFilter.setOp(CompositeFilter.Operator.AND); - for (FieldFilter fieldFilter : options.getFieldFilters()) { - compositeFilter.addFilters(fieldFilter.toProto()); - } - filter.setCompositeFilter(compositeFilter.build()); - structuredQuery.setWhere(filter.build()); + // There's an implicit AND operation between the top-level query filters. + if (!options.getFilters().isEmpty()) { + FilterInternal filter = + new CompositeComparisonFilter(options.getFilters(), CompositeFilter.Operator.AND); + structuredQuery.setWhere(filter.toProto()); } if (!options.getFieldOrders().isEmpty()) { @@ -1409,16 +1498,15 @@ private static Query fromProto(FirestoreRpcContext rpcContext, RunQueryReques queryOptions.setAllDescendants(structuredQuery.getFrom(0).getAllDescendants()); if (structuredQuery.hasWhere()) { - Filter where = structuredQuery.getWhere(); - if (where.hasCompositeFilter()) { - CompositeFilter compositeFilter = where.getCompositeFilter(); - ImmutableList.Builder fieldFilters = ImmutableList.builder(); - for (Filter filter : compositeFilter.getFiltersList()) { - fieldFilters.add(FieldFilter.fromProto(filter)); - } - queryOptions.setFieldFilters(fieldFilters.build()); + FilterInternal filter = FilterInternal.fromProto(structuredQuery.getWhere()); + + // There's an implicit AND operation between the top-level query filters. + if (filter instanceof CompositeComparisonFilter + && ((CompositeComparisonFilter) filter).isConjunction()) { + queryOptions.setFilters( + new ImmutableList.Builder().addAll(filter.getFilters()).build()); } else { - queryOptions.setFieldFilters(ImmutableList.of(FieldFilter.fromProto(where))); + queryOptions.setFilters(ImmutableList.of(filter)); } } diff --git a/google-cloud-firestore/src/test/java/com/google/cloud/firestore/QueryTest.java b/google-cloud-firestore/src/test/java/com/google/cloud/firestore/QueryTest.java index 061d345f6..d25104d22 100644 --- a/google-cloud-firestore/src/test/java/com/google/cloud/firestore/QueryTest.java +++ b/google-cloud-firestore/src/test/java/com/google/cloud/firestore/QueryTest.java @@ -47,7 +47,7 @@ import com.google.api.gax.rpc.ServerStreamingCallable; import com.google.cloud.Timestamp; import com.google.cloud.firestore.Query.ComparisonFilter; -import com.google.cloud.firestore.Query.FieldFilter; +import com.google.cloud.firestore.Query.FilterInternal; import com.google.cloud.firestore.spi.v1.FirestoreRpc; import com.google.common.io.BaseEncoding; import com.google.firestore.v1.ArrayValue; @@ -1320,15 +1320,11 @@ public void ensureFromProtoWorksWithAProxy() throws InvalidProtocolBufferExcepti ResourcePath path = query.options.getParentPath(); assertEquals("projects/test-project/databases/(default)/documents", path.getName()); assertEquals("testing-collection", query.options.getCollectionId()); - FieldFilter next = query.options.getFieldFilters().iterator().next(); - assertEquals("enabled", next.fieldReference.getFieldPath()); - - if (next instanceof ComparisonFilter) { - ComparisonFilter comparisonFilter = (ComparisonFilter) next; - assertFalse(comparisonFilter.isInequalityFilter()); - assertEquals(Value.newBuilder().setBooleanValue(true).build(), comparisonFilter.value); - } else { - fail("expect filter to be a comparison filter"); - } + FilterInternal next = query.options.getFilters().iterator().next(); + assertTrue(next instanceof ComparisonFilter); + ComparisonFilter comparisonFilter = (ComparisonFilter) next; + assertEquals("enabled", comparisonFilter.fieldReference.getFieldPath()); + assertFalse(comparisonFilter.isInequalityFilter()); + assertEquals(Value.newBuilder().setBooleanValue(true).build(), comparisonFilter.value); } } From 0d172cadbad96a5d90d7f5496f45327d8d4b45f9 Mon Sep 17 00:00:00 2001 From: Owl Bot Date: Wed, 22 Jun 2022 22:47:15 +0000 Subject: [PATCH 02/13] =?UTF-8?q?=F0=9F=A6=89=20Updates=20from=20OwlBot=20?= =?UTF-8?q?post-processor?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit See https://github.com/googleapis/repo-automation-bots/blob/main/packages/owl-bot/README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index f9bc56635..7170b364a 100644 --- a/README.md +++ b/README.md @@ -49,7 +49,7 @@ If you are using Maven without BOM, add this to your dependencies: If you are using Gradle 5.x or later, add this to your dependencies ```Groovy -implementation platform('com.google.cloud:libraries-bom:25.3.0') +implementation platform('com.google.cloud:libraries-bom:25.4.0') implementation 'com.google.cloud:google-cloud-firestore' ``` From 3559e8433539c3ed48db8ca4dd5dfb608e806337 Mon Sep 17 00:00:00 2001 From: Ehsan Nasiri Date: Wed, 22 Jun 2022 17:52:44 -0500 Subject: [PATCH 03/13] Minor fixes. --- .../main/java/com/google/cloud/firestore/Query.java | 13 ++++--------- 1 file changed, 4 insertions(+), 9 deletions(-) diff --git a/google-cloud-firestore/src/main/java/com/google/cloud/firestore/Query.java b/google-cloud-firestore/src/main/java/com/google/cloud/firestore/Query.java index 7238b7df8..0a1d98ae3 100644 --- a/google-cloud-firestore/src/main/java/com/google/cloud/firestore/Query.java +++ b/google-cloud-firestore/src/main/java/com/google/cloud/firestore/Query.java @@ -37,7 +37,6 @@ import com.google.api.gax.rpc.StreamController; import com.google.auto.value.AutoValue; import com.google.cloud.Timestamp; -import com.google.cloud.firestore.Filter.UnaryFilter; import com.google.cloud.firestore.Query.QueryOptions.Builder; import com.google.cloud.firestore.v1.FirestoreSettings; import com.google.common.base.Preconditions; @@ -102,14 +101,14 @@ StructuredQuery.Direction getDirection() { abstract static class FilterInternal { /** Returns a list of all field filters that are contained within this filter */ - public abstract List getFlattenedFilters(); + abstract List getFlattenedFilters(); /** Returns a list of all filters that are contained within this filter */ - public abstract List getFilters(); + abstract List getFilters(); /** Returns the field of the first filter that's an inequality, or null if none. */ @Nullable - public abstract FieldReference getFirstInequalityField(); + abstract FieldReference getFirstInequalityField(); /** Returns the proto representation of this filter */ abstract Filter toProto(); @@ -172,10 +171,6 @@ public FieldReference getFirstInequalityField() { return null; } - public StructuredQuery.CompositeFilter.Operator getOperator() { - return operator; - } - public boolean isConjunction() { return operator == CompositeFilter.Operator.AND; } @@ -194,7 +189,7 @@ public List getFlattenedFilters() { @Override Filter toProto() { - // A composite filter that only contains one sub-filter is equivalent to the sub-filter. + // A composite filter that contains one sub-filter is equivalent to the sub-filter. if (filters.size() == 1) { return filters.get(0).toProto(); } From c682e8fca66dec31c3e679a4fc0c685d373c024e Mon Sep 17 00:00:00 2001 From: Ehsan Nasiri Date: Wed, 22 Jun 2022 17:53:14 -0500 Subject: [PATCH 04/13] Remove commented code. --- .../src/main/java/com/google/cloud/firestore/Query.java | 5 ----- 1 file changed, 5 deletions(-) diff --git a/google-cloud-firestore/src/main/java/com/google/cloud/firestore/Query.java b/google-cloud-firestore/src/main/java/com/google/cloud/firestore/Query.java index 0a1d98ae3..dcc3f2c14 100644 --- a/google-cloud-firestore/src/main/java/com/google/cloud/firestore/Query.java +++ b/google-cloud-firestore/src/main/java/com/google/cloud/firestore/Query.java @@ -369,8 +369,6 @@ abstract static class QueryOptions { abstract @Nullable Cursor getEndCursor(); - // abstract ImmutableList getFieldFilters(); - abstract ImmutableList getFilters(); abstract ImmutableList getFieldOrders(); @@ -391,7 +389,6 @@ static Builder builder() { .setAllDescendants(false) .setLimitType(LimitType.First) .setFieldOrders(ImmutableList.of()) - // .setFieldFilters(ImmutableList.of()) .setFilters(ImmutableList.of()) .setFieldProjections(ImmutableList.of()) .setKindless(false) @@ -418,8 +415,6 @@ abstract static class Builder { abstract Builder setEndCursor(@Nullable Cursor value); - // abstract Builder setFieldFilters(ImmutableList value); - abstract Builder setFilters(ImmutableList value); abstract Builder setFieldOrders(ImmutableList value); From f7ae234fbc2472aa2e9f20f07c3fdf0a56cda36c Mon Sep 17 00:00:00 2001 From: Ehsan Nasiri Date: Wed, 22 Jun 2022 18:06:09 -0500 Subject: [PATCH 05/13] Better names? --- .../com/google/cloud/firestore/Query.java | 56 ++++++++++--------- .../com/google/cloud/firestore/QueryTest.java | 6 +- 2 files changed, 32 insertions(+), 30 deletions(-) diff --git a/google-cloud-firestore/src/main/java/com/google/cloud/firestore/Query.java b/google-cloud-firestore/src/main/java/com/google/cloud/firestore/Query.java index dcc3f2c14..727680942 100644 --- a/google-cloud-firestore/src/main/java/com/google/cloud/firestore/Query.java +++ b/google-cloud-firestore/src/main/java/com/google/cloud/firestore/Query.java @@ -101,7 +101,7 @@ StructuredQuery.Direction getDirection() { abstract static class FilterInternal { /** Returns a list of all field filters that are contained within this filter */ - abstract List getFlattenedFilters(); + abstract List getFlattenedFilters(); /** Returns a list of all filters that are contained within this filter */ abstract List getFilters(); @@ -115,12 +115,12 @@ abstract static class FilterInternal { static FilterInternal fromProto(StructuredQuery.Filter filter) { if (filter.hasUnaryFilter()) { - return new Query.UnaryFilter( + return new UnaryFilterInternal( filter.getUnaryFilter().getField(), filter.getUnaryFilter().getOp()); } if (filter.hasFieldFilter()) { - return new ComparisonFilter( + return new ComparisonFilterInternal( filter.getFieldFilter().getField(), filter.getFieldFilter().getOp(), filter.getFieldFilter().getValue()); @@ -137,19 +137,19 @@ static FilterInternal fromProto(StructuredQuery.Filter filter) { for (StructuredQuery.Filter subfilter : compositeFilter.getFiltersList()) { filters.add(FilterInternal.fromProto(subfilter)); } - return new CompositeComparisonFilter(filters, compositeFilter.getOp()); + return new CompositeFilterInternal(filters, compositeFilter.getOp()); } } - static class CompositeComparisonFilter extends FilterInternal { + static class CompositeFilterInternal extends FilterInternal { private final List filters; private final StructuredQuery.CompositeFilter.Operator operator; // Memoized list of all field filters that can be found by traversing the tree of filters // contained in this composite filter. - private List memoizedFlattenedFilters; + private List memoizedFlattenedFilters; - public CompositeComparisonFilter( + public CompositeFilterInternal( List filters, StructuredQuery.CompositeFilter.Operator operator) { this.filters = filters; this.operator = operator; @@ -163,7 +163,7 @@ public List getFilters() { @Nullable @Override public FieldReference getFirstInequalityField() { - for (FieldFilter fieldFilter : getFlattenedFilters()) { + for (FieldFilterInternal fieldFilter : getFlattenedFilters()) { if (fieldFilter.isInequalityFilter()) { return fieldFilter.fieldReference; } @@ -176,7 +176,7 @@ public boolean isConjunction() { } @Override - public List getFlattenedFilters() { + public List getFlattenedFilters() { if (memoizedFlattenedFilters != null) { return memoizedFlattenedFilters; } @@ -206,10 +206,10 @@ Filter toProto() { } } - abstract static class FieldFilter extends FilterInternal { + abstract static class FieldFilterInternal extends FilterInternal { protected final FieldReference fieldReference; - FieldFilter(FieldReference fieldReference) { + FieldFilterInternal(FieldReference fieldReference) { this.fieldReference = fieldReference; } @@ -220,16 +220,17 @@ public List getFilters() { } @Override - public List getFlattenedFilters() { + public List getFlattenedFilters() { return Collections.singletonList(this); } } - private static class UnaryFilter extends FieldFilter { + private static class UnaryFilterInternal extends FieldFilterInternal { private final StructuredQuery.UnaryFilter.Operator operator; - UnaryFilter(FieldReference fieldReference, StructuredQuery.UnaryFilter.Operator operator) { + UnaryFilterInternal( + FieldReference fieldReference, StructuredQuery.UnaryFilter.Operator operator) { super(fieldReference); this.operator = operator; } @@ -256,20 +257,20 @@ public boolean equals(Object o) { if (this == o) { return true; } - if (!(o instanceof UnaryFilter)) { + if (!(o instanceof UnaryFilterInternal)) { return false; } - UnaryFilter other = (UnaryFilter) o; + UnaryFilterInternal other = (UnaryFilterInternal) o; return Objects.equals(fieldReference, other.fieldReference) && Objects.equals(operator, other.operator); } } - static class ComparisonFilter extends FieldFilter { + static class ComparisonFilterInternal extends FieldFilterInternal { final StructuredQuery.FieldFilter.Operator operator; final Value value; - ComparisonFilter( + ComparisonFilterInternal( FieldReference fieldReference, StructuredQuery.FieldFilter.Operator operator, Value value) { super(fieldReference); this.value = value; @@ -305,10 +306,10 @@ public boolean equals(Object o) { if (this == o) { return true; } - if (!(o instanceof ComparisonFilter)) { + if (!(o instanceof ComparisonFilterInternal)) { return false; } - ComparisonFilter other = (ComparisonFilter) o; + ComparisonFilterInternal other = (ComparisonFilterInternal) o; return Objects.equals(fieldReference, other.fieldReference) && Objects.equals(operator, other.operator) && Objects.equals(value, other.value); @@ -911,7 +912,7 @@ FilterInternal parseFilter(com.google.cloud.firestore.Filter filter) { return parseCompositeFilter((com.google.cloud.firestore.Filter.CompositeFilter) filter); } - private FieldFilter parseFieldFilter( + private FieldFilterInternal parseFieldFilter( com.google.cloud.firestore.Filter.UnaryFilter fieldFilterData) { Object value = fieldFilterData.getValue(); Operator operator = fieldFilterData.getOperator(); @@ -926,7 +927,7 @@ private FieldFilter parseFieldFilter( : (value == null ? StructuredQuery.UnaryFilter.Operator.IS_NOT_NULL : StructuredQuery.UnaryFilter.Operator.IS_NOT_NAN); - return new UnaryFilter(fieldPath.toProto(), unaryOp); + return new UnaryFilterInternal(fieldPath.toProto(), unaryOp); } else { if (fieldPath.equals(FieldPath.DOCUMENT_ID)) { if (operator == ARRAY_CONTAINS || operator == ARRAY_CONTAINS_ANY) { @@ -951,7 +952,8 @@ private FieldFilter parseFieldFilter( value = this.convertReference(value); } } - return new ComparisonFilter(fieldPath.toProto(), operator, encodeValue(fieldPath, value)); + return new ComparisonFilterInternal( + fieldPath.toProto(), operator, encodeValue(fieldPath, value)); } } @@ -970,7 +972,7 @@ private FilterInternal parseCompositeFilter( if (parsedFilters.size() == 1) { return parsedFilters.get(0); } - return new CompositeComparisonFilter(parsedFilters, compositeFilterData.getOperator()); + return new CompositeFilterInternal(parsedFilters, compositeFilterData.getOperator()); } /** @@ -1360,7 +1362,7 @@ private StructuredQuery.Builder buildWithoutClientTranslation() { // There's an implicit AND operation between the top-level query filters. if (!options.getFilters().isEmpty()) { FilterInternal filter = - new CompositeComparisonFilter(options.getFilters(), CompositeFilter.Operator.AND); + new CompositeFilterInternal(options.getFilters(), CompositeFilter.Operator.AND); structuredQuery.setWhere(filter.toProto()); } @@ -1491,8 +1493,8 @@ private static Query fromProto(FirestoreRpcContext rpcContext, RunQueryReques FilterInternal filter = FilterInternal.fromProto(structuredQuery.getWhere()); // There's an implicit AND operation between the top-level query filters. - if (filter instanceof CompositeComparisonFilter - && ((CompositeComparisonFilter) filter).isConjunction()) { + if (filter instanceof CompositeFilterInternal + && ((CompositeFilterInternal) filter).isConjunction()) { queryOptions.setFilters( new ImmutableList.Builder().addAll(filter.getFilters()).build()); } else { diff --git a/google-cloud-firestore/src/test/java/com/google/cloud/firestore/QueryTest.java b/google-cloud-firestore/src/test/java/com/google/cloud/firestore/QueryTest.java index d25104d22..a8f8ac86d 100644 --- a/google-cloud-firestore/src/test/java/com/google/cloud/firestore/QueryTest.java +++ b/google-cloud-firestore/src/test/java/com/google/cloud/firestore/QueryTest.java @@ -46,7 +46,7 @@ import com.google.api.gax.rpc.ResponseObserver; import com.google.api.gax.rpc.ServerStreamingCallable; import com.google.cloud.Timestamp; -import com.google.cloud.firestore.Query.ComparisonFilter; +import com.google.cloud.firestore.Query.ComparisonFilterInternal; import com.google.cloud.firestore.Query.FilterInternal; import com.google.cloud.firestore.spi.v1.FirestoreRpc; import com.google.common.io.BaseEncoding; @@ -1321,8 +1321,8 @@ public void ensureFromProtoWorksWithAProxy() throws InvalidProtocolBufferExcepti assertEquals("projects/test-project/databases/(default)/documents", path.getName()); assertEquals("testing-collection", query.options.getCollectionId()); FilterInternal next = query.options.getFilters().iterator().next(); - assertTrue(next instanceof ComparisonFilter); - ComparisonFilter comparisonFilter = (ComparisonFilter) next; + assertTrue(next instanceof ComparisonFilterInternal); + ComparisonFilterInternal comparisonFilter = (ComparisonFilterInternal) next; assertEquals("enabled", comparisonFilter.fieldReference.getFieldPath()); assertFalse(comparisonFilter.isInequalityFilter()); assertEquals(Value.newBuilder().setBooleanValue(true).build(), comparisonFilter.value); From 34fc24c038c4dc29263944f8035ec947834e02ac Mon Sep 17 00:00:00 2001 From: Ehsan Nasiri Date: Thu, 23 Jun 2022 10:46:15 -0500 Subject: [PATCH 06/13] Update license format. --- .../com/google/cloud/firestore/Filter.java | 28 ++++++++++--------- 1 file changed, 15 insertions(+), 13 deletions(-) diff --git a/google-cloud-firestore/src/main/java/com/google/cloud/firestore/Filter.java b/google-cloud-firestore/src/main/java/com/google/cloud/firestore/Filter.java index 340667597..57a000961 100644 --- a/google-cloud-firestore/src/main/java/com/google/cloud/firestore/Filter.java +++ b/google-cloud-firestore/src/main/java/com/google/cloud/firestore/Filter.java @@ -1,16 +1,18 @@ -// Copyright 2021 Google LLC -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. +/* + * Copyright 2022 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ package com.google.cloud.firestore; From cb79fd0bebf57f0d348f647d189a63b8070000aa Mon Sep 17 00:00:00 2001 From: Ehsan Nasiri Date: Thu, 23 Jun 2022 11:24:05 -0500 Subject: [PATCH 07/13] Prefer `equals` to `==` for enum comparison. --- .../src/main/java/com/google/cloud/firestore/Query.java | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/google-cloud-firestore/src/main/java/com/google/cloud/firestore/Query.java b/google-cloud-firestore/src/main/java/com/google/cloud/firestore/Query.java index 727680942..cb36eff9b 100644 --- a/google-cloud-firestore/src/main/java/com/google/cloud/firestore/Query.java +++ b/google-cloud-firestore/src/main/java/com/google/cloud/firestore/Query.java @@ -918,9 +918,9 @@ private FieldFilterInternal parseFieldFilter( Operator operator = fieldFilterData.getOperator(); FieldPath fieldPath = fieldFilterData.getField(); - if ((operator == EQUAL || operator == NOT_EQUAL) && isUnaryComparison(value)) { + if ((operator.equals(EQUAL) || operator.equals(NOT_EQUAL)) && isUnaryComparison(value)) { StructuredQuery.UnaryFilter.Operator unaryOp = - operator == EQUAL + operator.equals(EQUAL) ? (value == null ? StructuredQuery.UnaryFilter.Operator.IS_NULL : StructuredQuery.UnaryFilter.Operator.IS_NAN) @@ -930,12 +930,12 @@ private FieldFilterInternal parseFieldFilter( return new UnaryFilterInternal(fieldPath.toProto(), unaryOp); } else { if (fieldPath.equals(FieldPath.DOCUMENT_ID)) { - if (operator == ARRAY_CONTAINS || operator == ARRAY_CONTAINS_ANY) { + if (operator.equals(ARRAY_CONTAINS) || operator.equals(ARRAY_CONTAINS_ANY)) { throw new IllegalArgumentException( String.format( "Invalid query. You cannot perform '%s' queries on FieldPath.documentId().", operator.toString())); - } else if (operator == IN | operator == NOT_IN) { + } else if (operator.equals(IN) || operator.equals(NOT_IN)) { if (!(value instanceof List) || ((List) value).isEmpty()) { throw new IllegalArgumentException( String.format( From 343edf2fbaac917f48c7bae37b463273a454153a Mon Sep 17 00:00:00 2001 From: Ehsan Nasiri Date: Thu, 23 Jun 2022 11:28:24 -0500 Subject: [PATCH 08/13] We must throw an error if Null or NaN is used in anything other than == or !=. --- .../com/google/cloud/firestore/Query.java | 26 ++++++++++++------- 1 file changed, 16 insertions(+), 10 deletions(-) diff --git a/google-cloud-firestore/src/main/java/com/google/cloud/firestore/Query.java b/google-cloud-firestore/src/main/java/com/google/cloud/firestore/Query.java index cb36eff9b..85c9fa210 100644 --- a/google-cloud-firestore/src/main/java/com/google/cloud/firestore/Query.java +++ b/google-cloud-firestore/src/main/java/com/google/cloud/firestore/Query.java @@ -918,16 +918,22 @@ private FieldFilterInternal parseFieldFilter( Operator operator = fieldFilterData.getOperator(); FieldPath fieldPath = fieldFilterData.getField(); - if ((operator.equals(EQUAL) || operator.equals(NOT_EQUAL)) && isUnaryComparison(value)) { - StructuredQuery.UnaryFilter.Operator unaryOp = - operator.equals(EQUAL) - ? (value == null - ? StructuredQuery.UnaryFilter.Operator.IS_NULL - : StructuredQuery.UnaryFilter.Operator.IS_NAN) - : (value == null - ? StructuredQuery.UnaryFilter.Operator.IS_NOT_NULL - : StructuredQuery.UnaryFilter.Operator.IS_NOT_NAN); - return new UnaryFilterInternal(fieldPath.toProto(), unaryOp); + if (isUnaryComparison(value)) { + if (operator.equals(EQUAL) || operator.equals(NOT_EQUAL)) { + StructuredQuery.UnaryFilter.Operator unaryOp = + operator.equals(EQUAL) + ? (value == null + ? StructuredQuery.UnaryFilter.Operator.IS_NULL + : StructuredQuery.UnaryFilter.Operator.IS_NAN) + : (value == null + ? StructuredQuery.UnaryFilter.Operator.IS_NOT_NULL + : StructuredQuery.UnaryFilter.Operator.IS_NOT_NAN); + return new UnaryFilterInternal(fieldPath.toProto(), unaryOp); + } else { + throw new IllegalArgumentException( + String.format( + "Cannot use '%s' in field comparison. Use an equality filter instead.", value)); + } } else { if (fieldPath.equals(FieldPath.DOCUMENT_ID)) { if (operator.equals(ARRAY_CONTAINS) || operator.equals(ARRAY_CONTAINS_ANY)) { From fe058d31554338797a5bcf7aaa7e78d5623d712e Mon Sep 17 00:00:00 2001 From: Ehsan Nasiri Date: Thu, 23 Jun 2022 12:40:47 -0500 Subject: [PATCH 09/13] Use Nunnull, not NonNull. --- .../com/google/cloud/firestore/Filter.java | 88 +++++++++---------- 1 file changed, 44 insertions(+), 44 deletions(-) diff --git a/google-cloud-firestore/src/main/java/com/google/cloud/firestore/Filter.java b/google-cloud-firestore/src/main/java/com/google/cloud/firestore/Filter.java index 57a000961..0e25187dd 100644 --- a/google-cloud-firestore/src/main/java/com/google/cloud/firestore/Filter.java +++ b/google-cloud-firestore/src/main/java/com/google/cloud/firestore/Filter.java @@ -20,8 +20,8 @@ import com.google.firestore.v1.StructuredQuery.FieldFilter.Operator; import java.util.Arrays; import java.util.List; +import javax.annotation.Nonnull; import javax.annotation.Nullable; -import org.checkerframework.checker.nullness.qual.NonNull; /** @hide */ public class Filter { @@ -55,7 +55,7 @@ static class CompositeFilter extends Filter { private final StructuredQuery.CompositeFilter.Operator operator; public CompositeFilter( - @NonNull List filters, StructuredQuery.CompositeFilter.Operator operator) { + @Nonnull List filters, StructuredQuery.CompositeFilter.Operator operator) { this.filters = filters; this.operator = operator; } @@ -69,114 +69,114 @@ public StructuredQuery.CompositeFilter.Operator getOperator() { } } - @NonNull - public static Filter equalTo(@NonNull String field, @Nullable Object value) { + @Nonnull + public static Filter equalTo(@Nonnull String field, @Nullable Object value) { return equalTo(FieldPath.fromDotSeparatedString(field), value); } - @NonNull - public static Filter equalTo(@NonNull FieldPath fieldPath, @Nullable Object value) { + @Nonnull + public static Filter equalTo(@Nonnull FieldPath fieldPath, @Nullable Object value) { return new UnaryFilter(fieldPath, Operator.EQUAL, value); } - @NonNull - public static Filter notEqualTo(@NonNull String field, @Nullable Object value) { + @Nonnull + public static Filter notEqualTo(@Nonnull String field, @Nullable Object value) { return notEqualTo(FieldPath.fromDotSeparatedString(field), value); } - @NonNull - public static Filter notEqualTo(@NonNull FieldPath fieldPath, @Nullable Object value) { + @Nonnull + public static Filter notEqualTo(@Nonnull FieldPath fieldPath, @Nullable Object value) { return new UnaryFilter(fieldPath, Operator.NOT_EQUAL, value); } - @NonNull - public static Filter greaterThan(@NonNull String field, @Nullable Object value) { + @Nonnull + public static Filter greaterThan(@Nonnull String field, @Nullable Object value) { return greaterThan(FieldPath.fromDotSeparatedString(field), value); } - @NonNull - public static Filter greaterThan(@NonNull FieldPath fieldPath, @Nullable Object value) { + @Nonnull + public static Filter greaterThan(@Nonnull FieldPath fieldPath, @Nullable Object value) { return new UnaryFilter(fieldPath, Operator.GREATER_THAN, value); } - @NonNull - public static Filter greaterThanOrEqualTo(@NonNull String field, @Nullable Object value) { + @Nonnull + public static Filter greaterThanOrEqualTo(@Nonnull String field, @Nullable Object value) { return greaterThanOrEqualTo(FieldPath.fromDotSeparatedString(field), value); } - @NonNull - public static Filter greaterThanOrEqualTo(@NonNull FieldPath fieldPath, @Nullable Object value) { + @Nonnull + public static Filter greaterThanOrEqualTo(@Nonnull FieldPath fieldPath, @Nullable Object value) { return new UnaryFilter(fieldPath, Operator.GREATER_THAN_OR_EQUAL, value); } - @NonNull - public static Filter lessThan(@NonNull String field, @Nullable Object value) { + @Nonnull + public static Filter lessThan(@Nonnull String field, @Nullable Object value) { return lessThan(FieldPath.fromDotSeparatedString(field), value); } - @NonNull - public static Filter lessThan(@NonNull FieldPath fieldPath, @Nullable Object value) { + @Nonnull + public static Filter lessThan(@Nonnull FieldPath fieldPath, @Nullable Object value) { return new UnaryFilter(fieldPath, Operator.LESS_THAN, value); } - @NonNull - public static Filter lessThanOrEqualTo(@NonNull String field, @Nullable Object value) { + @Nonnull + public static Filter lessThanOrEqualTo(@Nonnull String field, @Nullable Object value) { return lessThanOrEqualTo(FieldPath.fromDotSeparatedString(field), value); } - @NonNull - public static Filter lessThanOrEqualTo(@NonNull FieldPath fieldPath, @Nullable Object value) { + @Nonnull + public static Filter lessThanOrEqualTo(@Nonnull FieldPath fieldPath, @Nullable Object value) { return new UnaryFilter(fieldPath, Operator.LESS_THAN_OR_EQUAL, value); } - @NonNull - public static Filter arrayContains(@NonNull String field, @Nullable Object value) { + @Nonnull + public static Filter arrayContains(@Nonnull String field, @Nullable Object value) { return arrayContains(FieldPath.fromDotSeparatedString(field), value); } - @NonNull - public static Filter arrayContains(@NonNull FieldPath fieldPath, @Nullable Object value) { + @Nonnull + public static Filter arrayContains(@Nonnull FieldPath fieldPath, @Nullable Object value) { return new UnaryFilter(fieldPath, Operator.ARRAY_CONTAINS, value); } - @NonNull - public static Filter arrayContainsAny(@NonNull String field, @Nullable Object value) { + @Nonnull + public static Filter arrayContainsAny(@Nonnull String field, @Nullable Object value) { return arrayContainsAny(FieldPath.fromDotSeparatedString(field), value); } - @NonNull - public static Filter arrayContainsAny(@NonNull FieldPath fieldPath, @Nullable Object value) { + @Nonnull + public static Filter arrayContainsAny(@Nonnull FieldPath fieldPath, @Nullable Object value) { return new UnaryFilter(fieldPath, Operator.ARRAY_CONTAINS_ANY, value); } - @NonNull - public static Filter inArray(@NonNull String field, @Nullable Object value) { + @Nonnull + public static Filter inArray(@Nonnull String field, @Nullable Object value) { return inArray(FieldPath.fromDotSeparatedString(field), value); } - @NonNull - public static Filter inArray(@NonNull FieldPath fieldPath, @Nullable Object value) { + @Nonnull + public static Filter inArray(@Nonnull FieldPath fieldPath, @Nullable Object value) { return new UnaryFilter(fieldPath, Operator.IN, value); } - @NonNull - public static Filter notInArray(@NonNull String field, @Nullable Object value) { + @Nonnull + public static Filter notInArray(@Nonnull String field, @Nullable Object value) { return notInArray(FieldPath.fromDotSeparatedString(field), value); } - @NonNull - public static Filter notInArray(@NonNull FieldPath fieldPath, @Nullable Object value) { + @Nonnull + public static Filter notInArray(@Nonnull FieldPath fieldPath, @Nullable Object value) { return new UnaryFilter(fieldPath, Operator.NOT_IN, value); } - @NonNull + @Nonnull public static Filter or(Filter... filters) { // TODO(orquery): Change this to Operator.OR once it is available. return new CompositeFilter( Arrays.asList(filters), StructuredQuery.CompositeFilter.Operator.OPERATOR_UNSPECIFIED); } - @NonNull + @Nonnull public static Filter and(Filter... filters) { return new CompositeFilter( Arrays.asList(filters), StructuredQuery.CompositeFilter.Operator.AND); From 8cea870b2dae50cfc861b8738ceb192ab0b2ecf8 Mon Sep 17 00:00:00 2001 From: Ehsan Nasiri Date: Fri, 24 Jun 2022 15:59:49 -0500 Subject: [PATCH 10/13] Add tests. --- .../cloud/firestore/LocalFirestoreHelper.java | 26 +++++ .../com/google/cloud/firestore/QueryTest.java | 101 ++++++++++++++++++ 2 files changed, 127 insertions(+) diff --git a/google-cloud-firestore/src/test/java/com/google/cloud/firestore/LocalFirestoreHelper.java b/google-cloud-firestore/src/test/java/com/google/cloud/firestore/LocalFirestoreHelper.java index 38f853e7e..219c5702f 100644 --- a/google-cloud-firestore/src/test/java/com/google/cloud/firestore/LocalFirestoreHelper.java +++ b/google-cloud-firestore/src/test/java/com/google/cloud/firestore/LocalFirestoreHelper.java @@ -574,6 +574,32 @@ public static StructuredQuery filter( return filter(operator, path, string(value)); } + public static StructuredQuery.Filter fieldFilter( + String path, StructuredQuery.FieldFilter.Operator operator, String value) { + StructuredQuery.FieldFilter.Builder builder = + FieldFilter.newBuilder() + .setField(StructuredQuery.FieldReference.newBuilder().setFieldPath(path)) + .setOp(operator) + .setValue(Value.newBuilder().setStringValue(value).build()); + return StructuredQuery.Filter.newBuilder().setFieldFilter(builder).build(); + } + + public static StructuredQuery.Filter andFilters(StructuredQuery.Filter... filters) { + return compositeFilter(CompositeFilter.Operator.AND, Arrays.asList(filters)); + } + + public static StructuredQuery.Filter orFilters(StructuredQuery.Filter... filters) { + // TODO(orquery): Replace this with Operator.OR once it's available. + return compositeFilter(CompositeFilter.Operator.OPERATOR_UNSPECIFIED, Arrays.asList(filters)); + } + + private static StructuredQuery.Filter compositeFilter( + StructuredQuery.CompositeFilter.Operator operator, List filters) { + StructuredQuery.CompositeFilter.Builder builder = + StructuredQuery.CompositeFilter.newBuilder().setOp(operator).addAllFilters(filters); + return StructuredQuery.Filter.newBuilder().setCompositeFilter(builder).build(); + } + public static StructuredQuery filter( StructuredQuery.FieldFilter.Operator operator, String path, Value value) { StructuredQuery.Builder structuredQuery = StructuredQuery.newBuilder(); diff --git a/google-cloud-firestore/src/test/java/com/google/cloud/firestore/QueryTest.java b/google-cloud-firestore/src/test/java/com/google/cloud/firestore/QueryTest.java index a8f8ac86d..c98f05a2c 100644 --- a/google-cloud-firestore/src/test/java/com/google/cloud/firestore/QueryTest.java +++ b/google-cloud-firestore/src/test/java/com/google/cloud/firestore/QueryTest.java @@ -16,14 +16,18 @@ package com.google.cloud.firestore; +import static com.google.cloud.firestore.Filter.*; import static com.google.cloud.firestore.LocalFirestoreHelper.COLLECTION_ID; import static com.google.cloud.firestore.LocalFirestoreHelper.DOCUMENT_NAME; import static com.google.cloud.firestore.LocalFirestoreHelper.DOCUMENT_PATH; import static com.google.cloud.firestore.LocalFirestoreHelper.SINGLE_FIELD_SNAPSHOT; +import static com.google.cloud.firestore.LocalFirestoreHelper.andFilters; import static com.google.cloud.firestore.LocalFirestoreHelper.endAt; +import static com.google.cloud.firestore.LocalFirestoreHelper.fieldFilter; import static com.google.cloud.firestore.LocalFirestoreHelper.filter; import static com.google.cloud.firestore.LocalFirestoreHelper.limit; import static com.google.cloud.firestore.LocalFirestoreHelper.offset; +import static com.google.cloud.firestore.LocalFirestoreHelper.orFilters; import static com.google.cloud.firestore.LocalFirestoreHelper.order; import static com.google.cloud.firestore.LocalFirestoreHelper.query; import static com.google.cloud.firestore.LocalFirestoreHelper.queryResponse; @@ -54,6 +58,7 @@ import com.google.firestore.v1.RunQueryRequest; import com.google.firestore.v1.RunQueryResponse; import com.google.firestore.v1.StructuredQuery; +import com.google.firestore.v1.StructuredQuery.CollectionSelector; import com.google.firestore.v1.StructuredQuery.Direction; import com.google.firestore.v1.StructuredQuery.FieldFilter.Operator; import com.google.firestore.v1.Value; @@ -330,6 +335,42 @@ public void withFieldPathFilter() throws Exception { } } + @Test + public void withCompositeFilter() throws Exception { + doAnswer(queryResponse()) + .when(firestoreMock) + .streamRequest( + runQuery.capture(), + streamObserverCapture.capture(), + Matchers.any()); + + // a == 10 && (b==20 || c==30 || (d==40 && e>50) || f==60) + query + .where( + and( + equalTo("a", "10"), + or( + equalTo("b", "20"), + equalTo("c", "30"), + and(equalTo("d", "40"), greaterThan("e", "50")), + and(equalTo("f", "60")), + or(and())))) + .get() + .get(); + + StructuredQuery.Filter a = fieldFilter("a", Operator.EQUAL, "10"); + StructuredQuery.Filter b = fieldFilter("b", Operator.EQUAL, "20"); + StructuredQuery.Filter c = fieldFilter("c", Operator.EQUAL, "30"); + StructuredQuery.Filter d = fieldFilter("d", Operator.EQUAL, "40"); + StructuredQuery.Filter e = fieldFilter("e", Operator.GREATER_THAN, "50"); + StructuredQuery.Filter f = fieldFilter("f", Operator.EQUAL, "60"); + StructuredQuery.Builder structuredQuery = StructuredQuery.newBuilder(); + structuredQuery.setWhere(andFilters(a, orFilters(b, c, andFilters(d, e), f))); + structuredQuery.addFrom(CollectionSelector.newBuilder().setCollectionId("coll").build()); + + assertEquals(structuredQuery.build(), runQuery.getValue().getStructuredQuery()); + } + @Test public void inQueriesWithReferenceArray() throws Exception { doAnswer(queryResponse()) @@ -1260,6 +1301,66 @@ public void serializationTest() { assertSerialization(query); } + @Test + public void serializationTestWithEmptyCompositeFilter() { + assertSerialization(query); + query.where(or()); + assertSerialization(query); + query.where(and()); + assertSerialization(query); + query.where(and(or(and(or())))); + assertSerialization(query); + } + + @Test + public void serializationTestWithSingleFilterCompositeFilters() { + // Test the special handling of a composite filter that has only 1 filter inside it. Such filter + // is equivalent to its sub-filter. For example: AND(a==10) is the same as a==10. + assertSerialization(query); + // a == 10 + query.where(or(equalTo("a", 10))); + assertSerialization(query); + + // b > 20 + query.where(and(greaterThan("b", 20))); + assertSerialization(query); + + // c == 30 + query.where(or(and(or(and(equalTo("c", 30)))))); + assertSerialization(query); + } + + @Test + public void serializationTestWithNestedCompositeFilters() { + assertSerialization(query); + // a IN [1,2] + query.where(inArray("a", Arrays.asList(1, 2))); + assertSerialization(query); + // a IN [1,2] && (b==20 || c==30 || (d==40 && e>50)) || f==60 + query.where( + or( + equalTo("b", 20), + equalTo("c", 30), + and(equalTo("d", 40), greaterThan("e", 50)), + and(equalTo("f", 60)), + or(and()))); + assertSerialization(query); + query = query.orderBy("l"); + assertSerialization(query); + query = query.startAt("o"); + assertSerialization(query); + query = query.startAfter("p"); + assertSerialization(query); + query = query.endBefore("q"); + assertSerialization(query); + query = query.endAt("r"); + assertSerialization(query); + query = query.limit(8); + assertSerialization(query); + query = query.offset(9); + assertSerialization(query); + } + private void assertSerialization(Query query) { RunQueryRequest runQueryRequest = query.toProto(); Query deserializedQuery = Query.fromProto(firestoreMock, runQueryRequest); From 267da67bb32444ae402946bf44e387282c5c4d28 Mon Sep 17 00:00:00 2001 From: Ehsan Nasiri Date: Tue, 5 Jul 2022 15:30:09 -0500 Subject: [PATCH 11/13] Remove unrelated comment. --- .../src/main/java/com/google/cloud/firestore/Query.java | 1 - 1 file changed, 1 deletion(-) diff --git a/google-cloud-firestore/src/main/java/com/google/cloud/firestore/Query.java b/google-cloud-firestore/src/main/java/com/google/cloud/firestore/Query.java index 85c9fa210..65115c443 100644 --- a/google-cloud-firestore/src/main/java/com/google/cloud/firestore/Query.java +++ b/google-cloud-firestore/src/main/java/com/google/cloud/firestore/Query.java @@ -277,7 +277,6 @@ static class ComparisonFilterInternal extends FieldFilterInternal { this.operator = operator; } - // TODO(ehsan): This seems out of date. @Override boolean isInequalityFilter() { return operator.equals(GREATER_THAN) From c26af93ff5772305ef04f400c0da53f078832842 Mon Sep 17 00:00:00 2001 From: Owl Bot Date: Mon, 11 Jul 2022 18:48:05 +0000 Subject: [PATCH 12/13] =?UTF-8?q?=F0=9F=A6=89=20Updates=20from=20OwlBot=20?= =?UTF-8?q?post-processor?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit See https://github.com/googleapis/repo-automation-bots/blob/main/packages/owl-bot/README.md --- README.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 7170b364a..1e35d4990 100644 --- a/README.md +++ b/README.md @@ -49,20 +49,20 @@ If you are using Maven without BOM, add this to your dependencies: If you are using Gradle 5.x or later, add this to your dependencies ```Groovy -implementation platform('com.google.cloud:libraries-bom:25.4.0') +implementation platform('com.google.cloud:libraries-bom:26.0.0') implementation 'com.google.cloud:google-cloud-firestore' ``` If you are using Gradle without BOM, add this to your dependencies ```Groovy -implementation 'com.google.cloud:google-cloud-firestore:3.2.0' +implementation 'com.google.cloud:google-cloud-firestore:3.3.0' ``` If you are using SBT, add this to your dependencies ```Scala -libraryDependencies += "com.google.cloud" % "google-cloud-firestore" % "3.2.0" +libraryDependencies += "com.google.cloud" % "google-cloud-firestore" % "3.3.0" ``` ## Authentication From 3093c5675c19cb102c925f68cbb2e99e33e7444b Mon Sep 17 00:00:00 2001 From: Owl Bot Date: Mon, 11 Jul 2022 18:52:16 +0000 Subject: [PATCH 13/13] =?UTF-8?q?=F0=9F=A6=89=20Updates=20from=20OwlBot=20?= =?UTF-8?q?post-processor?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit See https://github.com/googleapis/repo-automation-bots/blob/main/packages/owl-bot/README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 4fa2b6c02..82685dad9 100644 --- a/README.md +++ b/README.md @@ -49,7 +49,7 @@ If you are using Maven without BOM, add this to your dependencies: If you are using Gradle 5.x or later, add this to your dependencies ```Groovy -implementation platform('com.google.cloud:libraries-bom:25.4.0') +implementation platform('com.google.cloud:libraries-bom:26.0.0') implementation 'com.google.cloud:google-cloud-firestore' ```