All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.google.cloud.firestore.Query Maven / Gradle / Ivy

There is a newer version: 3.29.1
Show newest version
/*
 * Copyright 2017 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 static com.google.firestore.v1.StructuredQuery.FieldFilter.Operator.ARRAY_CONTAINS;
import static com.google.firestore.v1.StructuredQuery.FieldFilter.Operator.ARRAY_CONTAINS_ANY;
import static com.google.firestore.v1.StructuredQuery.FieldFilter.Operator.EQUAL;
import static com.google.firestore.v1.StructuredQuery.FieldFilter.Operator.GREATER_THAN;
import static com.google.firestore.v1.StructuredQuery.FieldFilter.Operator.GREATER_THAN_OR_EQUAL;
import static com.google.firestore.v1.StructuredQuery.FieldFilter.Operator.IN;
import static com.google.firestore.v1.StructuredQuery.FieldFilter.Operator.LESS_THAN;
import static com.google.firestore.v1.StructuredQuery.FieldFilter.Operator.LESS_THAN_OR_EQUAL;
import static com.google.firestore.v1.StructuredQuery.FieldFilter.Operator.NOT_EQUAL;
import static com.google.firestore.v1.StructuredQuery.FieldFilter.Operator.NOT_IN;

import com.google.api.core.ApiFuture;
import com.google.api.core.InternalExtensionOnly;
import com.google.api.core.SettableApiFuture;
import com.google.api.gax.rpc.ApiStreamObserver;
import com.google.auto.value.AutoValue;
import com.google.cloud.Timestamp;
import com.google.cloud.firestore.Query.QueryOptions.Builder;
import com.google.cloud.firestore.encoding.CustomClassMapper;
import com.google.common.base.Preconditions;
import com.google.common.collect.ImmutableList;
import com.google.firestore.bundle.BundledQuery;
import com.google.firestore.v1.Cursor;
import com.google.firestore.v1.Document;
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.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;
import com.google.firestore.v1.Value;
import com.google.protobuf.ByteString;
import com.google.protobuf.Int32Value;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Objects;
import java.util.SortedSet;
import java.util.TreeSet;
import java.util.concurrent.Executor;
import java.util.logging.Logger;
import javax.annotation.Nonnull;
import javax.annotation.Nullable;

/**
 * A Query which you can read or listen to. You can also construct refined Query objects by adding
 * filters and ordering.
 */
@InternalExtensionOnly
public class Query extends StreamableQuery {

  static final Comparator DOCUMENT_ID_COMPARATOR =
      QueryDocumentSnapshot::compareDocumentId;
  private static final Logger LOGGER = Logger.getLogger(Query.class.getName());

  /** The direction of a sort. */
  public enum Direction {
    ASCENDING(StructuredQuery.Direction.ASCENDING, DOCUMENT_ID_COMPARATOR),
    DESCENDING(StructuredQuery.Direction.DESCENDING, DOCUMENT_ID_COMPARATOR.reversed());

    private final StructuredQuery.Direction direction;
    private final Comparator documentIdComparator;

    Direction(
        StructuredQuery.Direction direction,
        Comparator documentIdComparator) {
      this.direction = direction;
      this.documentIdComparator = documentIdComparator;
    }

    StructuredQuery.Direction getDirection() {
      return direction;
    }
  }

  abstract static class FilterInternal {
    /** Returns a list of all field filters that are contained within this filter */
    abstract List getFlattenedFilters();

    /** Returns a list of all filters that are contained within this filter */
    abstract List getFilters();

    /** Returns the field of the first filter that's an inequality, or null if none. */
    @Nullable
    abstract FieldReference getFirstInequalityField();

    /** Returns the proto representation of this filter */
    abstract Filter toProto();

    static FilterInternal fromProto(StructuredQuery.Filter filter) {
      if (filter.hasUnaryFilter()) {
        return new UnaryFilterInternal(
            filter.getUnaryFilter().getField(), filter.getUnaryFilter().getOp());
      }

      if (filter.hasFieldFilter()) {
        return new ComparisonFilterInternal(
            filter.getFieldFilter().getField(),
            filter.getFieldFilter().getOp(),
            filter.getFieldFilter().getValue());
      }

      // `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 CompositeFilterInternal(filters, compositeFilter.getOp());
    }
  }

  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;

    public CompositeFilterInternal(
        List filters, StructuredQuery.CompositeFilter.Operator operator) {
      this.filters = filters;
      this.operator = operator;
    }

    @Override
    public List getFilters() {
      return filters;
    }

    @Nullable
    @Override
    public FieldReference getFirstInequalityField() {
      for (FieldFilterInternal fieldFilter : getFlattenedFilters()) {
        if (fieldFilter.isInequalityFilter()) {
          return fieldFilter.fieldReference;
        }
      }
      return null;
    }

    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 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 FieldFilterInternal extends FilterInternal {
    protected final FieldReference fieldReference;

    FieldFilterInternal(FieldReference fieldReference) {
      this.fieldReference = fieldReference;
    }

    abstract boolean isInequalityFilter();

    public List getFilters() {
      return Collections.singletonList(this);
    }

    @Override
    public List getFlattenedFilters() {
      return Collections.singletonList(this);
    }
  }

  private static class UnaryFilterInternal extends FieldFilterInternal {

    private final StructuredQuery.UnaryFilter.Operator operator;

    UnaryFilterInternal(
        FieldReference fieldReference, StructuredQuery.UnaryFilter.Operator operator) {
      super(fieldReference);
      this.operator = operator;
    }

    @Override
    boolean isInequalityFilter() {
      return false;
    }

    @Nullable
    @Override
    public FieldReference getFirstInequalityField() {
      return null;
    }

    Filter toProto() {
      Filter.Builder result = Filter.newBuilder();
      result.getUnaryFilterBuilder().setField(fieldReference).setOp(operator);
      return result.build();
    }

    @Override
    public boolean equals(Object o) {
      if (this == o) {
        return true;
      }
      if (!(o instanceof UnaryFilterInternal)) {
        return false;
      }
      UnaryFilterInternal other = (UnaryFilterInternal) o;
      return Objects.equals(fieldReference, other.fieldReference)
          && Objects.equals(operator, other.operator);
    }
  }

  static class ComparisonFilterInternal extends FieldFilterInternal {
    final StructuredQuery.FieldFilter.Operator operator;
    final Value value;

    ComparisonFilterInternal(
        FieldReference fieldReference, StructuredQuery.FieldFilter.Operator operator, Value value) {
      super(fieldReference);
      this.value = value;
      this.operator = operator;
    }

    @Override
    boolean isInequalityFilter() {
      return operator.equals(GREATER_THAN)
          || operator.equals(GREATER_THAN_OR_EQUAL)
          || operator.equals(LESS_THAN)
          || operator.equals(LESS_THAN_OR_EQUAL)
          || operator.equals(NOT_EQUAL)
          || operator.equals(NOT_IN);
    }

    @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);
      return result.build();
    }

    @Override
    public boolean equals(Object o) {
      if (this == o) {
        return true;
      }
      if (!(o instanceof ComparisonFilterInternal)) {
        return false;
      }
      ComparisonFilterInternal other = (ComparisonFilterInternal) o;
      return Objects.equals(fieldReference, other.fieldReference)
          && Objects.equals(operator, other.operator)
          && Objects.equals(value, other.value);
    }
  }

  static final class FieldOrder implements Comparator {
    private final FieldReference fieldReference;
    private final Direction direction;

    FieldOrder(FieldReference fieldReference, Direction direction) {
      this.fieldReference = fieldReference;
      this.direction = direction;
    }

    FieldOrder(String field, Direction direction) {
      this.fieldReference = FieldPath.fromServerFormat(field).toProto();
      this.direction = direction;
    }

    Order toProto() {
      Order.Builder result = Order.newBuilder();
      result.setField(fieldReference);
      result.setDirection(direction.getDirection());
      return result.build();
    }

    public boolean equals(Object o) {
      if (this == o) {
        return true;
      }
      if (!(o instanceof FieldOrder)) {
        return false;
      }
      FieldOrder filter = (FieldOrder) o;
      if (direction != filter.direction) {
        return false;
      }
      return Objects.equals(fieldReference, filter.fieldReference);
    }

    public int compare(QueryDocumentSnapshot doc1, QueryDocumentSnapshot doc2) {
      String path = fieldReference.getFieldPath();
      if (FieldPath.isDocumentId(path)) {
        return direction.documentIdComparator.compare(doc1, doc2);
      }
      FieldPath fieldPath = FieldPath.fromDotSeparatedString(path);
      Preconditions.checkState(
          doc1.contains(fieldPath) && doc2.contains(fieldPath),
          "Can only compare fields that exist in the DocumentSnapshot."
              + " Please include the fields you are ordering on in your select() call.");
      Value v1 = doc1.extractField(fieldPath);
      Value v2 = doc2.extractField(fieldPath);

      int cmp = com.google.cloud.firestore.Order.INSTANCE.compare(v1, v2);
      return (direction == Direction.ASCENDING) ? cmp : -cmp;
    }
  }

  /** Denotes whether a provided limit is applied to the beginning or the end of the result set. */
  enum LimitType {
    First,
    Last
  }

  /** Options that define a Firestore Query. */
  @AutoValue
  abstract static class QueryOptions {

    abstract ResourcePath getParentPath();

    abstract String getCollectionId();

    abstract boolean getAllDescendants();

    abstract @Nullable Integer getLimit();

    abstract LimitType getLimitType();

    abstract @Nullable Integer getOffset();

    abstract @Nullable Cursor getStartCursor();

    abstract @Nullable Cursor getEndCursor();

    abstract ImmutableList getFilters();

    abstract ImmutableList getFieldOrders();

    abstract ImmutableList getFieldProjections();

    // Whether to select all documents under `parentPath`. By default, only
    // collections that match `collectionId` are selected.
    abstract boolean isKindless();

    // Whether to require consistent documents when restarting the query. By
    // default, restarting the query uses the readTime offset of the original
    // query to provide consistent results.
    abstract boolean getRequireConsistency();

    static Builder builder() {
      return new AutoValue_Query_QueryOptions.Builder()
          .setAllDescendants(false)
          .setLimitType(LimitType.First)
          .setFieldOrders(ImmutableList.of())
          .setFilters(ImmutableList.of())
          .setFieldProjections(ImmutableList.of())
          .setKindless(false)
          .setRequireConsistency(true);
    }

    abstract Builder toBuilder();

    @AutoValue.Builder
    abstract static class Builder {
      abstract Builder setParentPath(ResourcePath value);

      abstract Builder setCollectionId(String value);

      abstract Builder setAllDescendants(boolean value);

      abstract Builder setLimit(Integer value);

      abstract Builder setLimitType(LimitType value);

      abstract Builder setOffset(Integer value);

      abstract Builder setStartCursor(@Nullable Cursor value);

      abstract Builder setEndCursor(@Nullable Cursor value);

      abstract Builder setFilters(ImmutableList value);

      abstract Builder setFieldOrders(ImmutableList value);

      abstract Builder setFieldProjections(ImmutableList value);

      abstract Builder setKindless(boolean value);

      abstract Builder setRequireConsistency(boolean value);

      abstract QueryOptions build();
    }
  }

  /** Creates a query for documents in a single collection */
  Query(FirestoreRpcContext rpcContext, ResourcePath path) {
    this(
        rpcContext,
        QueryOptions.builder()
            .setParentPath(path.getParent())
            .setCollectionId(path.getId())
            .build());
  }

  protected Query(FirestoreRpcContext rpcContext, QueryOptions queryOptions) {
    super(rpcContext, queryOptions);
  }

  @Override
  QuerySnapshot createSnaphot(Timestamp readTime, final List documents) {
    return QuerySnapshot.withDocuments(this, readTime, documents);
  }

  /** Checks whether the provided object is NULL or NaN. */
  private static boolean isUnaryComparison(@Nullable Object value) {
    return value == null || value.equals(Double.NaN) || value.equals(Float.NaN);
  }

  /** Returns the sorted set of inequality filter fields used in this query. */
  private SortedSet getInequalityFilterFields() {
    SortedSet result = new TreeSet<>();

    for (FilterInternal filter : options.getFilters()) {
      for (FieldFilterInternal subFilter : filter.getFlattenedFilters()) {
        if (subFilter.isInequalityFilter()) {
          result.add(FieldPath.fromServerFormat(subFilter.fieldReference.getFieldPath()));
        }
      }
    }

    return result;
  }

  /** Computes the backend ordering semantics for DocumentSnapshot cursors. */
  ImmutableList createImplicitOrderBy() {
    // Any explicit order by fields should be added as is.
    List result = new ArrayList<>(options.getFieldOrders());

    HashSet fieldsNormalized = new HashSet<>();
    for (FieldOrder order : result) {
      fieldsNormalized.add(order.fieldReference.getFieldPath());
    }

    /** The order of the implicit ordering always matches the last explicit order by. */
    Direction lastDirection =
        result.isEmpty() ? Direction.ASCENDING : result.get(result.size() - 1).direction;

    /**
     * Any inequality fields not explicitly ordered should be implicitly ordered in a
     * lexicographical order. When there are multiple inequality filters on the same field, the
     * field should be added only once.
     *
     * 

Note: `SortedSet` sorts the key field before other fields. However, we want the * key field to be sorted last. */ SortedSet inequalityFields = getInequalityFilterFields(); for (FieldPath field : inequalityFields) { if (!fieldsNormalized.contains(field.toString()) && !FieldPath.isDocumentId(field.toString())) { result.add(new FieldOrder(field.toProto(), lastDirection)); } } // Add the document key field to the last if it is not explicitly ordered. if (!fieldsNormalized.contains(FieldPath.documentId().toString())) { result.add(new FieldOrder(FieldPath.documentId().toProto(), lastDirection)); } return ImmutableList.builder().addAll(result).build(); } private Cursor createCursor( ImmutableList order, DocumentSnapshot documentSnapshot, boolean before) { List fieldValues = new ArrayList<>(); for (FieldOrder fieldOrder : order) { String path = fieldOrder.fieldReference.getFieldPath(); if (FieldPath.isDocumentId(path)) { fieldValues.add(documentSnapshot.getReference()); } else { FieldPath fieldPath = FieldPath.fromServerFormat(path); Preconditions.checkArgument( documentSnapshot.contains(fieldPath), "Field '%s' is missing in the provided DocumentSnapshot. Please provide a document " + "that contains values for all specified orderBy() and where() constraints.", fieldPath); fieldValues.add(documentSnapshot.get(fieldPath)); } } return createCursor(order, fieldValues.toArray(), before); } private Cursor createCursor(List order, Object[] fieldValues, boolean before) { Cursor.Builder result = Cursor.newBuilder(); Preconditions.checkState( fieldValues.length != 0, "At least one cursor value must be specified."); Preconditions.checkState( fieldValues.length <= order.size(), "Too many cursor values specified. The specified values must match the " + "orderBy() constraints of the query."); Iterator fieldOrderIterator = order.iterator(); for (Object fieldValue : fieldValues) { Object sanitizedValue; FieldReference fieldReference = fieldOrderIterator.next().fieldReference; if (FieldPath.isDocumentId(fieldReference.getFieldPath())) { sanitizedValue = convertReference(fieldValue); } else { sanitizedValue = CustomClassMapper.serialize(fieldValue); } Value encodedValue = encodeValue(fieldReference, sanitizedValue); if (encodedValue == null) { throw FirestoreException.forInvalidArgument( "Cannot use FieldValue.delete() or FieldValue.serverTimestamp() in a query boundary"); } result.addValues(encodedValue); } result.setBefore(before); return result.build(); } /** * Validates that a value used with FieldValue.documentId() is either a string or a * DocumentReference that is part of the query`s result set. Throws a validation error or returns * a DocumentReference that can directly be used in the Query. */ private Object convertReference(Object fieldValue) { ResourcePath basePath = options.getAllDescendants() ? options.getParentPath() : options.getParentPath().append(options.getCollectionId()); DocumentReference reference; if (fieldValue instanceof String) { reference = new DocumentReference(rpcContext, basePath.append((String) fieldValue)); } else if (fieldValue instanceof DocumentReference) { reference = (DocumentReference) fieldValue; } else { throw new IllegalArgumentException( String.format( "The corresponding value for FieldPath.documentId() must be a String or a " + "DocumentReference, but was: %s.", fieldValue.toString())); } if (!basePath.isPrefixOf(reference.getResourcePath())) { throw new IllegalArgumentException( String.format( "'%s' is not part of the query result set and cannot be used as a query boundary.", reference.getPath())); } if (!options.getAllDescendants() && !reference.getParent().getResourcePath().equals(basePath)) { throw new IllegalArgumentException( String.format( "Only a direct child can be used as a query boundary. Found: '%s'", reference.getPath())); } return reference; } /** * Creates and returns a new Query with the additional filter that documents must contain the * specified field and the value should be equal to the specified value. * * @param field The name of the field to compare. * @param value The value for comparison. * @return The created Query. */ @Nonnull public Query whereEqualTo(@Nonnull String field, @Nullable Object value) { return whereEqualTo(FieldPath.fromDotSeparatedString(field), value); } /** * Creates and returns a new Query with the additional filter that documents must contain the * specified field and the value should be equal to the specified value. * * @param fieldPath The path of the field to compare. * @param value The value for comparison. * @return The created Query. */ @Nonnull public Query whereEqualTo(@Nonnull FieldPath fieldPath, @Nullable Object value) { return where(new com.google.cloud.firestore.Filter.UnaryFilter(fieldPath, EQUAL, value)); } /** * Creates and returns a new Query with the additional filter that documents must contain the * specified field and its value does not equal the specified value. * * @param field The name of the field to compare. * @param value The value for comparison. * @return The created Query. */ @Nonnull public Query whereNotEqualTo(@Nonnull String field, @Nullable Object value) { return whereNotEqualTo(FieldPath.fromDotSeparatedString(field), value); } /** * Creates and returns a new Query with the additional filter that documents must contain the * specified field and the value does not equal the specified value. * * @param fieldPath The path of the field to compare. * @param value The value for comparison. * @return The created Query. */ @Nonnull public Query whereNotEqualTo(@Nonnull FieldPath fieldPath, @Nullable Object value) { return where(new com.google.cloud.firestore.Filter.UnaryFilter(fieldPath, NOT_EQUAL, value)); } /** * Creates and returns a new Query with the additional filter that documents must contain the * specified field and the value should be less than the specified value. * * @param field The name of the field to compare. * @param value The value for comparison. * @return The created Query. */ @Nonnull public Query whereLessThan(@Nonnull String field, @Nonnull Object value) { return whereLessThan(FieldPath.fromDotSeparatedString(field), value); } /** * Creates and returns a new Query with the additional filter that documents must contain the * specified field and the value should be less than the specified value. * * @param fieldPath The path of the field to compare. * @param value The value for comparison. * @return The created Query. */ @Nonnull public Query whereLessThan(@Nonnull FieldPath fieldPath, @Nonnull Object value) { return where(new com.google.cloud.firestore.Filter.UnaryFilter(fieldPath, LESS_THAN, value)); } /** * Creates and returns a new Query with the additional filter that documents must contain the * specified field and the value should be less or equal to the specified value. * * @param field The name of the field to compare. * @param value The value for comparison. * @return The created Query. */ @Nonnull public Query whereLessThanOrEqualTo(@Nonnull String field, @Nonnull Object value) { return whereLessThanOrEqualTo(FieldPath.fromDotSeparatedString(field), value); } /** * Creates and returns a new Query with the additional filter that documents must contain the * specified field and the value should be less or equal to the specified value. * * @param fieldPath The path of the field to compare. * @param value The value for comparison. * @return The created Query. */ @Nonnull public Query whereLessThanOrEqualTo(@Nonnull FieldPath fieldPath, @Nonnull Object value) { return where( new com.google.cloud.firestore.Filter.UnaryFilter(fieldPath, LESS_THAN_OR_EQUAL, value)); } /** * Creates and returns a new Query with the additional filter that documents must contain the * specified field and the value should be greater than the specified value. * * @param field The name of the field to compare. * @param value The value for comparison. * @return The created Query. */ @Nonnull public Query whereGreaterThan(@Nonnull String field, @Nonnull Object value) { return whereGreaterThan(FieldPath.fromDotSeparatedString(field), value); } /** * Creates and returns a new Query with the additional filter that documents must contain the * specified field and the value should be greater than the specified value. * * @param fieldPath The path of the field to compare. * @param value The value for comparison. * @return The created Query. */ @Nonnull public Query whereGreaterThan(@Nonnull FieldPath fieldPath, @Nonnull Object value) { return where(new com.google.cloud.firestore.Filter.UnaryFilter(fieldPath, GREATER_THAN, value)); } /** * Creates and returns a new Query with the additional filter that documents must contain the * specified field and the value should be greater than or equal to the specified value. * * @param field The name of the field to compare. * @param value The value for comparison. * @return The created Query. */ @Nonnull public Query whereGreaterThanOrEqualTo(@Nonnull String field, @Nonnull Object value) { return whereGreaterThanOrEqualTo(FieldPath.fromDotSeparatedString(field), value); } /** * Creates and returns a new Query with the additional filter that documents must contain the * specified field and the value should be greater than or equal to the specified value. * * @param fieldPath The path of the field to compare. * @param value The value for comparison. * @return The created Query. */ @Nonnull public Query whereGreaterThanOrEqualTo(@Nonnull FieldPath fieldPath, @Nonnull Object value) { return where( new com.google.cloud.firestore.Filter.UnaryFilter(fieldPath, GREATER_THAN_OR_EQUAL, value)); } /** * Creates and returns a new Query with the additional filter that documents must contain the * specified field, the value must be an array, and that the array must contain the provided * value. * *

A Query can have only one whereArrayContains() filter and it cannot be combined with * whereArrayContainsAny(). * * @param field The name of the field containing an array to search * @param value The value that must be contained in the array * @return The created Query. */ @Nonnull public Query whereArrayContains(@Nonnull String field, @Nonnull Object value) { return whereArrayContains(FieldPath.fromDotSeparatedString(field), value); } /** * Creates and returns a new Query with the additional filter that documents must contain the * specified field, the value must be an array, and that the array must contain the provided * value. * *

A Query can have only one whereArrayContains() filter and it cannot be combined with * whereArrayContainsAny(). * * @param fieldPath The path of the field containing an array to search * @param value The value that must be contained in the array * @return The created Query. */ @Nonnull public Query whereArrayContains(@Nonnull FieldPath fieldPath, @Nonnull Object value) { return where( new com.google.cloud.firestore.Filter.UnaryFilter(fieldPath, ARRAY_CONTAINS, value)); } /** * Creates and returns a new Query with the additional filter that documents must contain the * specified field, the value must be an array, and that the array must contain at least one value * from the provided list. * *

A Query can have only one whereArrayContainsAny() filter and it cannot be combined with * whereArrayContains() or whereIn(). * * @param field The name of the field containing an array to search. * @param values A list that contains the values to match. * @return The created Query. */ @Nonnull public Query whereArrayContainsAny( @Nonnull String field, @Nonnull List values) { return whereArrayContainsAny(FieldPath.fromDotSeparatedString(field), values); } /** * Creates and returns a new Query with the additional filter that documents must contain the * specified field, the value must be an array, and that the array must contain at least one value * from the provided list. * *

A Query can have only one whereArrayContainsAny() filter and it cannot be combined with * whereArrayContains() or whereIn(). * * @param fieldPath The path of the field containing an array to search. * @param values A list that contains the values to match. * @return The created Query. */ @Nonnull public Query whereArrayContainsAny( @Nonnull FieldPath fieldPath, @Nonnull List values) { return where( new com.google.cloud.firestore.Filter.UnaryFilter(fieldPath, ARRAY_CONTAINS_ANY, values)); } /** * Creates and returns a new Query with the additional filter that documents must contain the * specified field and the value must equal one of the values from the provided list. * *

A Query can have only one whereIn() filter, and it cannot be combined with * whereArrayContainsAny(). * * @param field The name of the field to search. * @param values A list that contains the values to match. * @return The created Query. */ @Nonnull public Query whereIn(@Nonnull String field, @Nonnull List values) { return whereIn(FieldPath.fromDotSeparatedString(field), values); } /** * Creates and returns a new Query with the additional filter that documents must contain the * specified field and the value must equal one of the values from the provided list. * *

A Query can have only one whereIn() filter, and it cannot be combined with * whereArrayContainsAny(). * * @param fieldPath The path of the field to search. * @param values A list that contains the values to match. * @return The created Query. */ @Nonnull public Query whereIn(@Nonnull FieldPath fieldPath, @Nonnull List values) { return where(new com.google.cloud.firestore.Filter.UnaryFilter(fieldPath, IN, values)); } /** * Creates and returns a new Query with the additional filter that documents must contain the * specified field and the value does not equal any of the values from the provided list. * *

A Query can have only one whereNotIn() filter and it cannot be combined with * whereArrayContains(), whereArrayContainsAny(), whereIn(), or whereNotEqualTo(). * * @param field The name of the field to search. * @param values The list that contains the values to match. * @return The created Query. */ @Nonnull public Query whereNotIn(@Nonnull String field, @Nonnull List values) { return whereNotIn(FieldPath.fromDotSeparatedString(field), values); } /** * Creates and returns a new Query with the additional filter that documents must contain the * specified field and the value does not equal any of the values from the provided list. * *

A Query can have only one whereNotIn() filter, and it cannot be combined with * whereArrayContains(), whereArrayContainsAny(), whereIn(), or whereNotEqualTo(). * * @param fieldPath The path of the field to search. * @param values The list that contains the values to match. * @return The created Query. */ @Nonnull public Query whereNotIn(@Nonnull FieldPath fieldPath, @Nonnull List values) { return where(new com.google.cloud.firestore.Filter.UnaryFilter(fieldPath, NOT_IN, values)); } /** * Creates and returns a new Query with the additional filter. * * @param filter The new filter to apply to the existing query. * @return The newly created Query. */ public Query where(com.google.cloud.firestore.Filter filter) { Preconditions.checkState( options.getStartCursor() == null && options.getEndCursor() == null, "Cannot call a where() clause after defining a boundary with startAt(), " + "startAfter(), endBefore() or endAt()."); FilterInternal parsedFilter = parseFilter(filter); if (parsedFilter.getFilters().isEmpty()) { // Return the existing query if not adding any more filters (for example an empty composite // filter). return this; } Builder newOptions = options.toBuilder(); newOptions.setFilters(append(options.getFilters(), parsedFilter)); return new Query(rpcContext, newOptions.build()); } 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 FieldFilterInternal parseFieldFilter( com.google.cloud.firestore.Filter.UnaryFilter fieldFilterData) { Object value = fieldFilterData.getValue(); Operator operator = fieldFilterData.getOperator(); FieldPath fieldPath = fieldFilterData.getField(); 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)) { throw new IllegalArgumentException( String.format( "Invalid query. You cannot perform '%s' queries on FieldPath.documentId().", operator.toString())); } else if (operator.equals(IN) || operator.equals(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); } } return new ComparisonFilterInternal( fieldPath.toProto(), operator, encodeValue(fieldPath, value)); } } 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 CompositeFilterInternal(parsedFilters, compositeFilterData.getOperator()); } /** * Creates and returns a new Query that's additionally sorted by the specified field. * * @param field The field to sort by. * @return The created Query. */ @Nonnull public Query orderBy(@Nonnull String field) { return orderBy(FieldPath.fromDotSeparatedString(field), Direction.ASCENDING); } /** * Creates and returns a new Query that's additionally sorted by the specified field. * * @param fieldPath The field to sort by. * @return The created Query. */ @Nonnull public Query orderBy(@Nonnull FieldPath fieldPath) { return orderBy(fieldPath, Direction.ASCENDING); } /** * Creates and returns a new Query that's additionally sorted by the specified field, optionally * in descending order instead of ascending. * * @param field The field to sort by. * @param direction The direction to sort. * @return The created Query. */ @Nonnull public Query orderBy(@Nonnull String field, @Nonnull Direction direction) { return orderBy(FieldPath.fromDotSeparatedString(field), direction); } /** * Creates and returns a new Query that's additionally sorted by the specified field, optionally * in descending order instead of ascending. * * @param fieldPath The field to sort by. * @param direction The direction to sort. * @return The created Query. */ @Nonnull public Query orderBy(@Nonnull FieldPath fieldPath, @Nonnull Direction direction) { Preconditions.checkState( options.getStartCursor() == null && options.getEndCursor() == null, "Cannot specify an orderBy() constraint after calling startAt(), " + "startAfter(), endBefore() or endAt()."); Builder newOptions = options.toBuilder(); FieldOrder newFieldOrder = new FieldOrder(fieldPath.toProto(), direction); newOptions.setFieldOrders(append(options.getFieldOrders(), newFieldOrder)); return new Query(rpcContext, newOptions.build()); } /** * Creates and returns a new Query that only returns the first matching documents. * * @param limit The maximum number of items to return. * @return The created Query. */ @Nonnull public Query limit(int limit) { return new Query( rpcContext, options.toBuilder().setLimit(limit).setLimitType(LimitType.First).build()); } /** * Creates and returns a new Query that only returns the last matching documents. * *

You must specify at least one orderBy clause for limitToLast queries. Otherwise, an {@link * java.lang.IllegalStateException} is thrown during execution. * *

Results for limitToLast() queries are only available once all documents are received. Hence, * limitToLast() queries cannot be streamed via the {@link #stream(ApiStreamObserver)} API. * * @param limit the maximum number of items to return * @return the created Query */ @Nonnull public Query limitToLast(int limit) { return new Query( rpcContext, options.toBuilder().setLimit(limit).setLimitType(LimitType.Last).build()); } /** * Creates and returns a new Query that skips the first n results. * * @param offset The number of items to skip. * @return The created Query. */ @Nonnull public Query offset(int offset) { return new Query(rpcContext, options.toBuilder().setOffset(offset).build()); } /** * Creates and returns a new Query that starts at the provided document (inclusive). The starting * position is relative to the order of the query. The document must contain all of the fields * provided in the orderBy of this query. * * @param snapshot The snapshot of the document to start at. * @return The created Query. */ @Nonnull public Query startAt(@Nonnull DocumentSnapshot snapshot) { ImmutableList fieldOrders = createImplicitOrderBy(); Cursor cursor = createCursor(fieldOrders, snapshot, true); Builder newOptions = options.toBuilder(); newOptions.setFieldOrders(fieldOrders); newOptions.setStartCursor(cursor); return new Query(rpcContext, newOptions.build()); } /** * Creates and returns a new Query that starts at the provided fields relative to the order of the * query. The order of the field values must match the order of the order by clauses of the query. * * @param fieldValues The field values to start this query at, in order of the query's order by. * @return The created Query. */ @Nonnull public Query startAt(Object... fieldValues) { // TODO(b/296435819): Remove this warning message. warningOnSingleDocumentReference(fieldValues); ImmutableList fieldOrders = fieldValues.length == 1 && fieldValues[0] instanceof DocumentReference ? createImplicitOrderBy() : options.getFieldOrders(); Cursor cursor = createCursor(fieldOrders, fieldValues, true); Builder newOptions = options.toBuilder(); newOptions.setFieldOrders(fieldOrders); newOptions.setStartCursor(cursor); return new Query(rpcContext, newOptions.build()); } /** * Creates and returns a new Query instance that applies a field mask to the result and returns * the specified subset of fields. You can specify a list of field paths to return, or use an * empty list to only return the references of matching documents. * * @param fields The fields to include. * @return The created Query. */ @Nonnull public Query select(String... fields) { FieldPath[] fieldPaths = new FieldPath[fields.length]; for (int i = 0; i < fields.length; ++i) { fieldPaths[i] = FieldPath.fromDotSeparatedString(fields[i]); } return select(fieldPaths); } /** * Creates and returns a new Query instance that applies a field mask to the result and returns * the specified subset of fields. You can specify a list of field paths to return, or use an * empty list to only return the references of matching documents. * * @param fieldPaths The field paths to include. * @return The created Query. */ @Nonnull public Query select(FieldPath... fieldPaths) { if (fieldPaths.length == 0) { fieldPaths = new FieldPath[] {FieldPath.DOCUMENT_ID}; } ImmutableList.Builder fieldProjections = ImmutableList.builderWithExpectedSize(fieldPaths.length); for (FieldPath path : fieldPaths) { FieldReference fieldReference = FieldReference.newBuilder().setFieldPath(path.getEncodedPath()).build(); fieldProjections.add(fieldReference); } Builder newOptions = options.toBuilder().setFieldProjections(fieldProjections.build()); return new Query(rpcContext, newOptions.build()); } @Override boolean isRetryableWithCursor() { return true; } /** * Creates and returns a new Query that starts after the provided document (exclusive). The * starting position is relative to the order of the query. The document must contain all of the * fields provided in the orderBy of this query. * * @param snapshot The snapshot of the document to start after. * @return The created Query. */ @Nonnull @Override public Query startAfter(@Nonnull DocumentSnapshot snapshot) { ImmutableList fieldOrders = createImplicitOrderBy(); Cursor cursor = createCursor(fieldOrders, snapshot, false); Builder newOptions = options.toBuilder(); newOptions.setFieldOrders(fieldOrders); newOptions.setStartCursor(cursor); return new Query(rpcContext, newOptions.build()); } /** * Creates and returns a new Query that starts after the provided fields relative to the order of * the query. The order of the field values must match the order of the order by clauses of the * query. * * @param fieldValues The field values to start this query after, in order of the query's order * by. * @return The created Query. */ public Query startAfter(Object... fieldValues) { // TODO(b/296435819): Remove this warning message. warningOnSingleDocumentReference(fieldValues); ImmutableList fieldOrders = fieldValues.length == 1 && fieldValues[0] instanceof DocumentReference ? createImplicitOrderBy() : options.getFieldOrders(); Cursor cursor = createCursor(fieldOrders, fieldValues, false); Builder newOptions = options.toBuilder(); newOptions.setFieldOrders(fieldOrders); newOptions.setStartCursor(cursor); return new Query(rpcContext, newOptions.build()); } /** * Creates and returns a new Query that ends before the provided document (exclusive). The end * position is relative to the order of the query. The document must contain all of the fields * provided in the orderBy of this query. * * @param snapshot The snapshot of the document to end before. * @return The created Query. */ @Nonnull public Query endBefore(@Nonnull DocumentSnapshot snapshot) { ImmutableList fieldOrders = createImplicitOrderBy(); Cursor cursor = createCursor(fieldOrders, snapshot, true); Builder newOptions = options.toBuilder(); newOptions.setFieldOrders(fieldOrders); newOptions.setEndCursor(cursor); return new Query(rpcContext, newOptions.build()); } /** * Creates and returns a new Query that ends before the provided fields relative to the order of * the query. The order of the field values must match the order of the order by clauses of the * query. * * @param fieldValues The field values to end this query before, in order of the query's order by. * @return The created Query. */ @Nonnull public Query endBefore(Object... fieldValues) { // TODO(b/296435819): Remove this warning message. warningOnSingleDocumentReference(fieldValues); ImmutableList fieldOrders = fieldValues.length == 1 && fieldValues[0] instanceof DocumentReference ? createImplicitOrderBy() : options.getFieldOrders(); Cursor cursor = createCursor(fieldOrders, fieldValues, true); Builder newOptions = options.toBuilder(); newOptions.setFieldOrders(fieldOrders); newOptions.setEndCursor(cursor); return new Query(rpcContext, newOptions.build()); } /** * Creates and returns a new Query that ends at the provided fields relative to the order of the * query. The order of the field values must match the order of the order by clauses of the query. * * @param fieldValues The field values to end this query at, in order of the query's order by. * @return The created Query. */ @Nonnull public Query endAt(Object... fieldValues) { // TODO(b/296435819): Remove this warning message. warningOnSingleDocumentReference(fieldValues); ImmutableList fieldOrders = fieldValues.length == 1 && fieldValues[0] instanceof DocumentReference ? createImplicitOrderBy() : options.getFieldOrders(); Cursor cursor = createCursor(fieldOrders, fieldValues, false); Builder newOptions = options.toBuilder(); newOptions.setFieldOrders(fieldOrders); newOptions.setEndCursor(cursor); return new Query(rpcContext, newOptions.build()); } private void warningOnSingleDocumentReference(Object... fieldValues) { if (options.getFieldOrders().isEmpty() && fieldValues.length == 1 && fieldValues[0] instanceof DocumentReference) { LOGGER.warning( "Warning: Passing DocumentReference into a cursor without orderBy clause is not an intended " + "behavior. Please use DocumentSnapshot or add an explicit orderBy on document key field."); } } /** * Creates and returns a new Query that ends at the provided document (inclusive). The end * position is relative to the order of the query. The document must contain all of the fields * provided in the orderBy of this query. * * @param snapshot The snapshot of the document to end at. * @return The created Query. */ @Nonnull public Query endAt(@Nonnull DocumentSnapshot snapshot) { ImmutableList fieldOrders = createImplicitOrderBy(); Cursor cursor = createCursor(fieldOrders, snapshot, false); Builder newOptions = options.toBuilder(); newOptions.setFieldOrders(fieldOrders); newOptions.setEndCursor(cursor); return new Query(rpcContext, newOptions.build()); } /** Build the final Firestore query. */ StructuredQuery.Builder buildQuery() { StructuredQuery.Builder structuredQuery = buildWithoutClientTranslation(); if (options.getLimitType().equals(LimitType.Last)) { structuredQuery.clearOrderBy(); structuredQuery.clearStartAt(); structuredQuery.clearEndAt(); // Apply client translation for limitToLast. if (!options.getFieldOrders().isEmpty()) { for (FieldOrder order : options.getFieldOrders()) { // Flip the orderBy directions since we want the last results order = new FieldOrder( order.fieldReference, order.direction.equals(Direction.ASCENDING) ? Direction.DESCENDING : Direction.ASCENDING); structuredQuery.addOrderBy(order.toProto()); } } if (options.getStartCursor() != null) { // Swap the cursors to match the flipped query ordering. Cursor cursor = options .getStartCursor() .toBuilder() .setBefore(!options.getStartCursor().getBefore()) .build(); structuredQuery.setEndAt(cursor); } if (options.getEndCursor() != null) { // Swap the cursors to match the flipped query ordering. Cursor cursor = options .getEndCursor() .toBuilder() .setBefore(!options.getEndCursor().getBefore()) .build(); structuredQuery.setStartAt(cursor); } } return structuredQuery; } /** * Builds a {@link BundledQuery} that is able to be saved in a bundle file. * *

This will not perform any limitToLast order flip, as {@link BundledQuery} has first class * representation via {@link BundledQuery.LimitType}. */ BundledQuery toBundledQuery() { StructuredQuery.Builder structuredQuery = buildWithoutClientTranslation(); return BundledQuery.newBuilder() .setStructuredQuery(structuredQuery) .setParent(options.getParentPath().toString()) .setLimitType( options.getLimitType().equals(LimitType.Last) ? BundledQuery.LimitType.LAST : BundledQuery.LimitType.FIRST) .build(); } private StructuredQuery.Builder buildWithoutClientTranslation() { StructuredQuery.Builder structuredQuery = StructuredQuery.newBuilder(); CollectionSelector.Builder collectionSelector = CollectionSelector.newBuilder(); // Kindless queries select all descendant documents, so we don't add the collectionId field. if (!options.isKindless()) { collectionSelector.setCollectionId(options.getCollectionId()); } collectionSelector.setAllDescendants(options.getAllDescendants()); structuredQuery.addFrom(collectionSelector); // There's an implicit AND operation between the top-level query filters. if (!options.getFilters().isEmpty()) { FilterInternal filter = new CompositeFilterInternal(options.getFilters(), CompositeFilter.Operator.AND); structuredQuery.setWhere(filter.toProto()); } if (!options.getFieldOrders().isEmpty()) { for (FieldOrder order : options.getFieldOrders()) { structuredQuery.addOrderBy(order.toProto()); } } else if (LimitType.Last.equals(options.getLimitType())) { throw new IllegalStateException( "limitToLast() queries require specifying at least one orderBy() clause."); } if (!options.getFieldProjections().isEmpty()) { structuredQuery.getSelectBuilder().addAllFields(options.getFieldProjections()); } if (options.getLimit() != null) { structuredQuery.setLimit(Int32Value.newBuilder().setValue(options.getLimit())); } if (options.getOffset() != null) { structuredQuery.setOffset(options.getOffset()); } if (options.getStartCursor() != null) { structuredQuery.setStartAt(options.getStartCursor()); } if (options.getEndCursor() != null) { structuredQuery.setEndAt(options.getEndCursor()); } return structuredQuery; } /** * Executes the query and streams the results as a StreamObserver of DocumentSnapshots. * * @param responseObserver The observer to be notified when results arrive. */ public void stream(@Nonnull final ApiStreamObserver responseObserver) { Preconditions.checkState( !LimitType.Last.equals(Query.this.options.getLimitType()), "Query results for queries that include limitToLast() constraints cannot be streamed. " + "Use Query.get() instead."); internalStream( new ApiStreamObserver() { @Override public void onNext(RunQueryResponse runQueryResponse) { if (runQueryResponse.hasDocument()) { Document document = runQueryResponse.getDocument(); QueryDocumentSnapshot documentSnapshot = QueryDocumentSnapshot.fromDocument( rpcContext, Timestamp.fromProto(runQueryResponse.getReadTime()), document); responseObserver.onNext(documentSnapshot); } } @Override public void onError(Throwable throwable) { responseObserver.onError(throwable); } @Override public void onCompleted() { responseObserver.onCompleted(); } }, /* startTimeNanos= */ rpcContext.getClock().nanoTime(), /* transactionId= */ null, /* readTime= */ null, /* explainOptions= */ null, /* isRetryRequestWithCursor= */ false); } /** * Executes the query, streams the results as a StreamObserver of DocumentSnapshots, and returns * an ApiFuture that will be resolved with the associated {@link ExplainMetrics}. * * @param options The options that configure the explain request. * @param documentObserver The observer to be notified every time a new document arrives. */ @Nonnull public ApiFuture explainStream( @Nonnull ExplainOptions options, @Nonnull ApiStreamObserver documentObserver) { Preconditions.checkState( !LimitType.Last.equals(Query.this.options.getLimitType()), "Query results for queries that include limitToLast() constraints cannot be streamed. " + "Use Query.explain() instead."); final SettableApiFuture metricsFuture = SettableApiFuture.create(); internalStream( new ApiStreamObserver() { @Override public void onNext(RunQueryResponse runQueryResponse) { if (runQueryResponse.hasDocument()) { Document document = runQueryResponse.getDocument(); QueryDocumentSnapshot documentSnapshot = QueryDocumentSnapshot.fromDocument( rpcContext, Timestamp.fromProto(runQueryResponse.getReadTime()), document); documentObserver.onNext(documentSnapshot); } if (runQueryResponse.hasExplainMetrics()) { metricsFuture.set(new ExplainMetrics(runQueryResponse.getExplainMetrics())); } } @Override public void onError(Throwable throwable) { metricsFuture.setException(throwable); documentObserver.onError(throwable); } @Override public void onCompleted() { documentObserver.onCompleted(); if (!metricsFuture.isDone()) { // This means the gRPC stream completed without any metrics. metricsFuture.setException( new RuntimeException("Did not receive any explain results.")); } } }, /* startTimeNanos= */ rpcContext.getClock().nanoTime(), /* transactionId= */ null, /* readTime= */ null, /* explainOptions= */ options, /* isRetryRequestWithCursor= */ false); return metricsFuture; } /** * Returns the {@link RunQueryRequest} that this Query instance represents. The request contains * the serialized form of all Query constraints. * *

Runtime metadata (as required for `limitToLast()` queries) is not serialized and as such, * the serialized request will return the results in the original backend order. * * @return the serialized RunQueryRequest */ public RunQueryRequest toProto() { return toRunQueryRequestBuilder(null, null, null).build(); } @Override protected RunQueryRequest.Builder toRunQueryRequestBuilder( @Nullable final ByteString transactionId, @Nullable final Timestamp readTime, @Nullable ExplainOptions explainOptions) { // Builder for RunQueryRequest RunQueryRequest.Builder request = RunQueryRequest.newBuilder(); request.setStructuredQuery(buildQuery()); request.setParent(options.getParentPath().toString()); if (explainOptions != null) { request.setExplainOptions(explainOptions.toProto()); } if (transactionId != null) { request.setTransaction(transactionId); } if (readTime != null) { request.setReadTime(readTime.toProto()); } return request; } /** * Returns a Query instance that can be used to execute the provided {@link RunQueryRequest}. * *

Only RunQueryRequests that pertain to the same project as the Firestore instance can be * deserialized. * *

Runtime metadata (as required for `limitToLast()` queries) is not restored and as such, the * results for limitToLast() queries will be returned in the original backend order. * * @param firestore a Firestore instance to apply the query to * @param proto the serialized RunQueryRequest * @return a Query instance that can be used to execute the RunQueryRequest */ public static Query fromProto(Firestore firestore, RunQueryRequest proto) { Preconditions.checkState( FirestoreRpcContext.class.isAssignableFrom(firestore.getClass()), "The firestore instance passed to this method must also implement FirestoreRpcContext."); return fromProto((FirestoreRpcContext) firestore, proto); } private static Query fromProto(FirestoreRpcContext rpcContext, RunQueryRequest proto) { QueryOptions.Builder queryOptions = QueryOptions.builder(); StructuredQuery structuredQuery = proto.getStructuredQuery(); ResourcePath parentPath = ResourcePath.create(proto.getParent()); if (!rpcContext.getDatabaseName().equals(parentPath.getDatabaseName().toString())) { throw new IllegalArgumentException( String.format( "Cannot deserialize query from different Firestore project (\"%s\" vs \"%s\")", rpcContext.getDatabaseName(), parentPath.getDatabaseName())); } queryOptions.setParentPath(parentPath); Preconditions.checkArgument( structuredQuery.getFromCount() == 1, "Can only deserialize query with exactly one collection selector."); queryOptions.setCollectionId(structuredQuery.getFrom(0).getCollectionId()); queryOptions.setAllDescendants(structuredQuery.getFrom(0).getAllDescendants()); if (structuredQuery.hasWhere()) { FilterInternal filter = FilterInternal.fromProto(structuredQuery.getWhere()); // There's an implicit AND operation between the top-level query filters. if (filter instanceof CompositeFilterInternal && ((CompositeFilterInternal) filter).isConjunction()) { queryOptions.setFilters( new ImmutableList.Builder().addAll(filter.getFilters()).build()); } else { queryOptions.setFilters(ImmutableList.of(filter)); } } ImmutableList.Builder fieldOrders = ImmutableList.builderWithExpectedSize(structuredQuery.getOrderByCount()); for (Order order : structuredQuery.getOrderByList()) { fieldOrders.add( new FieldOrder(order.getField(), Direction.valueOf(order.getDirection().name()))); } queryOptions.setFieldOrders(fieldOrders.build()); if (structuredQuery.hasLimit()) { queryOptions.setLimit(structuredQuery.getLimit().getValue()); } if (structuredQuery.getOffset() != 0) { queryOptions.setOffset(structuredQuery.getOffset()); } if (structuredQuery.hasSelect()) { queryOptions.setFieldProjections( ImmutableList.copyOf(structuredQuery.getSelect().getFieldsList())); } if (structuredQuery.hasStartAt()) { queryOptions.setStartCursor(structuredQuery.getStartAt()); } if (structuredQuery.hasEndAt()) { queryOptions.setEndCursor(structuredQuery.getEndAt()); } return new Query(rpcContext, queryOptions.build()); } private Value encodeValue(FieldReference fieldReference, Object value) { return encodeValue(FieldPath.fromDotSeparatedString(fieldReference.getFieldPath()), value); } private Value encodeValue(FieldPath fieldPath, Object value) { Object sanitizedObject = CustomClassMapper.serialize(value); Value encodedValue = UserDataConverter.encodeValue(fieldPath, sanitizedObject, UserDataConverter.ARGUMENT); if (encodedValue == null) { throw FirestoreException.forInvalidArgument( "Cannot use Firestore sentinels in FieldFilter or cursors"); } return encodedValue; } /** * Executes the query and returns the results as QuerySnapshot. * * @return An ApiFuture that will be resolved with the results of the Query. */ @Override @Nonnull public ApiFuture get() { return get(null, null); } /** * Plans and optionally executes this query. Returns an ApiFuture that will be resolved with the * planner information, statistics from the query execution (if any), and the query results (if * any). * * @return An ApiFuture that will be resolved with the planner information, statistics from the * query execution (if any), and the query results (if any). */ @Override @Nonnull public ApiFuture> explain(ExplainOptions options) { return super.explain(options); } /** * Starts listening to this query. * * @param listener The event listener that will be called with the snapshots. * @return A registration object that can be used to remove the listener. */ @Nonnull public ListenerRegistration addSnapshotListener(@Nonnull EventListener listener) { return addSnapshotListener(rpcContext.getClient().getExecutor(), listener); } /** * Starts listening to this query. * * @param executor The executor to use to call the listener. * @param listener The event listener that will be called with the snapshots. * @return A registration object that can be used to remove the listener. */ @Nonnull public ListenerRegistration addSnapshotListener( @Nonnull Executor executor, @Nonnull EventListener listener) { return Watch.forQuery(this).runWatch(executor, listener); } Comparator comparator() { Iterator iterator = options.getFieldOrders().iterator(); if (!iterator.hasNext()) { return DOCUMENT_ID_COMPARATOR; } FieldOrder fieldOrder = iterator.next(); Comparator comparator = fieldOrder; while (iterator.hasNext()) { fieldOrder = iterator.next(); comparator = comparator.thenComparing(fieldOrder); } // Add implicit sorting by name, using the last specified direction. Direction lastDirection = fieldOrder.direction; return comparator.thenComparing(lastDirection.documentIdComparator); } /** * Helper method to append an element to an existing ImmutableList. Returns the newly created * list. */ private ImmutableList append(ImmutableList existingList, T newElement) { ImmutableList.Builder builder = ImmutableList.builderWithExpectedSize(existingList.size() + 1); builder.addAll(existingList); builder.add(newElement); return builder.build(); } /** * Returns a query that counts the documents in the result set of this query. * *

The returned query, when executed, counts the documents in the result set of this query * without actually downloading the documents. * *

Using the returned query to count the documents is efficient because only the final count, * not the documents' data, is downloaded. The returned query can count the documents in cases * where the result set is prohibitively large to download entirely (thousands of documents). * * @return a query that counts the documents in the result set of this query. */ @Nonnull public AggregateQuery count() { return new AggregateQuery(this, Collections.singletonList(AggregateField.count())); } /** * Calculates the specified aggregations over the documents in the result set of the given query * without actually downloading the documents. * *

Using the returned query to perform aggregations is efficient because only the final * aggregation values, not the documents' data, is downloaded. The returned query can perform * aggregations of the documents in cases where the result set is prohibitively large to download * entirely (thousands of documents). * * @return an {@link AggregateQuery} that performs aggregations on the documents in the result set * of this query. */ @Nonnull public AggregateQuery aggregate( @Nonnull AggregateField aggregateField1, @Nonnull AggregateField... aggregateFields) { List aggregateFieldList = new ArrayList<>(); aggregateFieldList.add(aggregateField1); aggregateFieldList.addAll(Arrays.asList(aggregateFields)); return new AggregateQuery(this, aggregateFieldList); } /** * Returns a VectorQuery that can perform vector distance (similarity) search with given * parameters. * *

The returned query, when executed, performs a distance (similarity) search on the specified * `vectorField` against the given `queryVector` and returns the top documents that are closest to * the `queryVector`. * *

Only documents whose `vectorField` field is a {@link VectorValue} of the same dimension as * `queryVector` participate in the query, all other documents are ignored. * *

{@code VectorQuery vectorQuery = col.findNearest("embedding", new double[] {41, 42}, 10, * VectorQuery.DistanceMeasure.EUCLIDEAN); QuerySnapshot querySnapshot = await * vectorQuery.get().get(); DocumentSnapshot mostSimilarDocument = * querySnapshot.getDocuments().get(0);} * * @param vectorField A string specifying the vector field to search on. * @param queryVector A representation of the vector used to measure the distance from * `vectorField` values in the documents. * @param limit The upper bound of documents to return, must be a positive integer with a maximum * value of 1000. * @param distanceMeasure What type of distance is calculated when performing the query. See * {@link VectorQuery.DistanceMeasure}. * @return an {@link VectorQuery} that performs vector distance (similarity) search with the given * parameters. */ public VectorQuery findNearest( String vectorField, double[] queryVector, int limit, VectorQuery.DistanceMeasure distanceMeasure) { return findNearest( FieldPath.fromDotSeparatedString(vectorField), FieldValue.vector(queryVector), limit, distanceMeasure, VectorQueryOptions.getDefaultInstance()); } /** * Returns a VectorQuery that can perform vector distance (similarity) search with given * parameters. * *

The returned query, when executed, performs a distance (similarity) search on the specified * `vectorField` against the given `queryVector` and returns the top documents that are closest to * the `queryVector`. * *

Only documents whose `vectorField` field is a {@link VectorValue} of the same dimension as * `queryVector` participate in the query, all other documents are ignored. * *

{@code VectorQuery vectorQuery = col.findNearest( "embedding", new double[] {41, 42}, 10, * VectorQuery.DistanceMeasure.EUCLIDEAN, * FindNearestOptions.newBuilder().setDistanceThreshold(0.11).setDistanceResultField("foo").build()); * QuerySnapshot querySnapshot = await vectorQuery.get().get(); DocumentSnapshot * mostSimilarDocument = querySnapshot.getDocuments().get(0);} * * @param vectorField A string specifying the vector field to search on. * @param queryVector A representation of the vector used to measure the distance from * `vectorField` values in the documents. * @param limit The upper bound of documents to return, must be a positive integer with a maximum * value of 1000. * @param distanceMeasure What type of distance is calculated when performing the query. See * {@link VectorQuery.DistanceMeasure}. * @param vectorQueryOptions Optional arguments for VectorQueries, see {@link VectorQueryOptions}. * @return an {@link VectorQuery} that performs vector distance (similarity) search with the given * parameters. */ public VectorQuery findNearest( String vectorField, double[] queryVector, int limit, VectorQuery.DistanceMeasure distanceMeasure, VectorQueryOptions vectorQueryOptions) { return findNearest( FieldPath.fromDotSeparatedString(vectorField), FieldValue.vector(queryVector), limit, distanceMeasure, vectorQueryOptions); } /** * Returns a VectorQuery that can perform vector distance (similarity) search with given * parameters. * *

The returned query, when executed, performs a distance (similarity) search on the specified * `vectorField` against the given `queryVector` and returns the top documents that are closest to * the `queryVector`. * *

Only documents whose `vectorField` field is a {@link VectorValue} of the same dimension as * `queryVector` participate in the query, all other documents are ignored. * *

{@code VectorValue queryVector = FieldValue.vector(new double[] {41, 42}); VectorQuery * vectorQuery = col.findNearest( FieldPath.of("embedding"), queryVector, 10, * VectorQuery.DistanceMeasure.EUCLIDEAN); QuerySnapshot querySnapshot = await * vectorQuery.get().get(); DocumentSnapshot mostSimilarDocument = * querySnapshot.getDocuments().get(0);} * * @param vectorField A {@link FieldPath} specifying the vector field to search on. * @param queryVector The {@link VectorValue} used to measure the distance from `vectorField` * values in the documents. * @param limit The upper bound of documents to return, must be a positive integer with a maximum * value of 1000. * @param distanceMeasure What type of distance is calculated when performing the query. See * {@link VectorQuery.DistanceMeasure}. * @return an {@link VectorQuery} that performs vector distance (similarity) search with the given * parameters. */ public VectorQuery findNearest( FieldPath vectorField, VectorValue queryVector, int limit, VectorQuery.DistanceMeasure distanceMeasure) { return findNearest( vectorField, queryVector, limit, distanceMeasure, VectorQueryOptions.getDefaultInstance()); } /** * Returns a VectorQuery that can perform vector distance (similarity) search with given * parameters. * *

The returned query, when executed, performs a distance (similarity) search on the specified * `vectorField` against the given `queryVector` and returns the top documents that are closest to * the `queryVector`. * *

Only documents whose `vectorField` field is a {@link VectorValue} of the same dimension as * `queryVector` participate in the query, all other documents are ignored. * *

{@code VectorQuery vectorQuery = col.findNearest( FieldPath.of("embedding"), queryVector, * 10, VectorQuery.DistanceMeasure.EUCLIDEAN, * FindNearestOptions.newBuilder().setDistanceThreshold(0.11).setDistanceResultField("foo").build()); * QuerySnapshot querySnapshot = await vectorQuery.get().get(); DocumentSnapshot * mostSimilarDocument = querySnapshot.getDocuments().get(0);} * * @param vectorField A {@link FieldPath} specifying the vector field to search on. * @param queryVector The {@link VectorValue} used to measure the distance from `vectorField` * values in the documents. * @param limit The upper bound of documents to return, must be a positive integer with a maximum * value of 1000. * @param distanceMeasure What type of distance is calculated when performing the query. See * {@link VectorQuery.DistanceMeasure}. * @param vectorQueryOptions Optional arguments for VectorQueries, see {@link VectorQueryOptions}. * @return an {@link VectorQuery} that performs vector distance (similarity) search with the given * parameters. */ public VectorQuery findNearest( FieldPath vectorField, VectorValue queryVector, int limit, VectorQuery.DistanceMeasure distanceMeasure, VectorQueryOptions vectorQueryOptions) { if (limit <= 0) { throw FirestoreException.forInvalidArgument( "Not a valid positive `limit` number. `limit` must be larger than 0."); } if (queryVector.size() == 0) { throw FirestoreException.forInvalidArgument( "Not a valid vector size. `queryVector` size must be larger than 0."); } return new VectorQuery( this, vectorField, queryVector, limit, distanceMeasure, vectorQueryOptions); } /** * Returns true if this Query is equal to the provided object. * * @param obj The object to compare against. * @return Whether this Query is equal to the provided object. */ @Override public boolean equals(Object obj) { if (this == obj) { return true; } if (obj == null || !(obj instanceof Query)) { return false; } Query query = (Query) obj; return Objects.equals(rpcContext, query.rpcContext) && Objects.equals(options, query.options); } @Override public int hashCode() { return Objects.hash(rpcContext, options); } }