diff --git a/.gitignore b/.gitignore index dfc6122dc..45a7fffc4 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,7 @@ target/ .idea/ +.idx/ +.vscode/ *.iml __pycache__/ .DS_Store diff --git a/.kokoro/nightly/integration-named-db.cfg b/.kokoro/nightly/integration-named-db.cfg index 7d0afbaed..1d3f31f63 100644 --- a/.kokoro/nightly/integration-named-db.cfg +++ b/.kokoro/nightly/integration-named-db.cfg @@ -13,7 +13,7 @@ env_vars: { env_vars: { key: "INTEGRATION_TEST_ARGS" - value: "-DFIRESTORE_NAMED_DATABASE=test-db" + value: "-DFIRESTORE_NAMED_DATABASE=test-db -DFIRESTORE_EDITION=standard" } # TODO: remove this after we've migrated all tests and scripts diff --git a/.kokoro/nightly/integration.cfg b/.kokoro/nightly/integration.cfg index f60aebf4b..5d2dffad2 100644 --- a/.kokoro/nightly/integration.cfg +++ b/.kokoro/nightly/integration.cfg @@ -10,6 +10,12 @@ env_vars: { key: "JOB_TYPE" value: "integration" } + +env_vars: { + key: "INTEGRATION_TEST_ARGS" + value: "-DFIRESTORE_EDITION=standard" +} + # TODO: remove this after we've migrated all tests and scripts env_vars: { key: "GCLOUD_PROJECT" diff --git a/.kokoro/presubmit/graalvm-a.cfg b/.kokoro/presubmit/graalvm-a.cfg index 35e57f557..11c421440 100644 --- a/.kokoro/presubmit/graalvm-a.cfg +++ b/.kokoro/presubmit/graalvm-a.cfg @@ -11,6 +11,11 @@ env_vars: { value: "graalvm" } +env_vars: { + key: "INTEGRATION_TEST_ARGS" + value: "-DFIRESTORE_EDITION=standard" +} + # TODO: remove this after we've migrated all tests and scripts env_vars: { key: "GCLOUD_PROJECT" @@ -35,4 +40,4 @@ env_vars: { env_vars: { key: "IT_SERVICE_ACCOUNT_EMAIL" value: "it-service-account@gcloud-devel.iam.gserviceaccount.com" -} \ No newline at end of file +} diff --git a/.kokoro/presubmit/graalvm-b.cfg b/.kokoro/presubmit/graalvm-b.cfg index 24accde47..2fb516651 100644 --- a/.kokoro/presubmit/graalvm-b.cfg +++ b/.kokoro/presubmit/graalvm-b.cfg @@ -11,6 +11,11 @@ env_vars: { value: "graalvm" } +env_vars: { + key: "INTEGRATION_TEST_ARGS" + value: "-DFIRESTORE_EDITION=standard" +} + # TODO: remove this after we've migrated all tests and scripts env_vars: { key: "GCLOUD_PROJECT" @@ -35,4 +40,4 @@ env_vars: { env_vars: { key: "IT_SERVICE_ACCOUNT_EMAIL" value: "it-service-account@gcloud-devel.iam.gserviceaccount.com" -} \ No newline at end of file +} diff --git a/.kokoro/presubmit/graalvm-c.cfg b/.kokoro/presubmit/graalvm-c.cfg index 01407e173..1554918af 100644 --- a/.kokoro/presubmit/graalvm-c.cfg +++ b/.kokoro/presubmit/graalvm-c.cfg @@ -11,6 +11,11 @@ env_vars: { value: "graalvm" } +env_vars: { + key: "INTEGRATION_TEST_ARGS" + value: "-DFIRESTORE_EDITION=standard" +} + # TODO: remove this after we've migrated all tests and scripts env_vars: { key: "GCLOUD_PROJECT" @@ -35,4 +40,4 @@ env_vars: { env_vars: { key: "IT_SERVICE_ACCOUNT_EMAIL" value: "it-service-account@gcloud-devel.iam.gserviceaccount.com" -} \ No newline at end of file +} diff --git a/.kokoro/presubmit/graalvm-native-a.cfg b/.kokoro/presubmit/graalvm-native-a.cfg index d2cba4c76..1e6e21370 100644 --- a/.kokoro/presubmit/graalvm-native-a.cfg +++ b/.kokoro/presubmit/graalvm-native-a.cfg @@ -11,6 +11,11 @@ env_vars: { value: "graalvm" } +env_vars: { + key: "INTEGRATION_TEST_ARGS" + value: "-DFIRESTORE_EDITION=standard" +} + env_vars: { key: "GOOGLE_CLOUD_PROJECT" value: "java-review" @@ -24,4 +29,4 @@ env_vars: { env_vars: { key: "SECRET_MANAGER_KEYS" value: "java-review_firestore-java-it" -} \ No newline at end of file +} diff --git a/.kokoro/presubmit/graalvm-native-b.cfg b/.kokoro/presubmit/graalvm-native-b.cfg index eb0fc34c6..1f79b0698 100644 --- a/.kokoro/presubmit/graalvm-native-b.cfg +++ b/.kokoro/presubmit/graalvm-native-b.cfg @@ -11,6 +11,11 @@ env_vars: { value: "graalvm" } +env_vars: { + key: "INTEGRATION_TEST_ARGS" + value: "-DFIRESTORE_EDITION=standard" +} + env_vars: { key: "GOOGLE_CLOUD_PROJECT" value: "java-review" @@ -24,4 +29,4 @@ env_vars: { env_vars: { key: "SECRET_MANAGER_KEYS" value: "java-review_firestore-java-it" -} \ No newline at end of file +} diff --git a/.kokoro/presubmit/graalvm-native-c.cfg b/.kokoro/presubmit/graalvm-native-c.cfg index a48dfa802..5dba283ef 100644 --- a/.kokoro/presubmit/graalvm-native-c.cfg +++ b/.kokoro/presubmit/graalvm-native-c.cfg @@ -11,6 +11,11 @@ env_vars: { value: "graalvm" } +env_vars: { + key: "INTEGRATION_TEST_ARGS" + value: "-DFIRESTORE_EDITION=standard" +} + env_vars: { key: "GOOGLE_CLOUD_PROJECT" value: "java-review" @@ -24,4 +29,4 @@ env_vars: { env_vars: { key: "SECRET_MANAGER_KEYS" value: "java-review_firestore-java-it" -} \ No newline at end of file +} diff --git a/.kokoro/presubmit/integration-named-db.cfg b/.kokoro/presubmit/integration-named-db.cfg index b66392e83..81bb77849 100644 --- a/.kokoro/presubmit/integration-named-db.cfg +++ b/.kokoro/presubmit/integration-named-db.cfg @@ -13,7 +13,7 @@ env_vars: { env_vars: { key: "INTEGRATION_TEST_ARGS" - value: "-DFIRESTORE_NAMED_DATABASE=test-db" + value: "-DFIRESTORE_NAMED_DATABASE=test-db -DFIRESTORE_EDITION=standard" } env_vars: { diff --git a/.kokoro/presubmit/integration.cfg b/.kokoro/presubmit/integration.cfg index 40ef5968d..accd9db7e 100644 --- a/.kokoro/presubmit/integration.cfg +++ b/.kokoro/presubmit/integration.cfg @@ -11,6 +11,11 @@ env_vars: { value: "integration" } +env_vars: { + key: "INTEGRATION_TEST_ARGS" + value: "-DFIRESTORE_EDITION=standard" +} + env_vars: { key: "GCLOUD_PROJECT" value: "java-review" diff --git a/README.md b/README.md index 976e6e3ee..485c87f7d 100644 --- a/README.md +++ b/README.md @@ -106,6 +106,7 @@ Samples are in the [`samples/`](https://github.com/googleapis/java-firestore/tre | Sample | Source Code | Try it | | --------------------------- | --------------------------------- | ------ | +| Pipeline Snippets | [source code](https://github.com/googleapis/java-firestore/blob/main/samples/preview-snippets/src/main/java/com/example/firestore/PipelineSnippets.java) | [![Open in Cloud Shell][shell_img]](https://console.cloud.google.com/cloudshell/open?git_repo=https://github.com/googleapis/java-firestore&page=editor&open_in_editor=samples/preview-snippets/src/main/java/com/example/firestore/PipelineSnippets.java) | | Quickstart | [source code](https://github.com/googleapis/java-firestore/blob/main/samples/snippets/src/main/java/com/example/firestore/Quickstart.java) | [![Open in Cloud Shell][shell_img]](https://console.cloud.google.com/cloudshell/open?git_repo=https://github.com/googleapis/java-firestore&page=editor&open_in_editor=samples/snippets/src/main/java/com/example/firestore/Quickstart.java) | | Example Firestore Beam Read | [source code](https://github.com/googleapis/java-firestore/blob/main/samples/snippets/src/main/java/com/example/firestore/beam/ExampleFirestoreBeamRead.java) | [![Open in Cloud Shell][shell_img]](https://console.cloud.google.com/cloudshell/open?git_repo=https://github.com/googleapis/java-firestore&page=editor&open_in_editor=samples/snippets/src/main/java/com/example/firestore/beam/ExampleFirestoreBeamRead.java) | | Example Firestore Beam Write | [source code](https://github.com/googleapis/java-firestore/blob/main/samples/snippets/src/main/java/com/example/firestore/beam/ExampleFirestoreBeamWrite.java) | [![Open in Cloud Shell][shell_img]](https://console.cloud.google.com/cloudshell/open?git_repo=https://github.com/googleapis/java-firestore&page=editor&open_in_editor=samples/snippets/src/main/java/com/example/firestore/beam/ExampleFirestoreBeamWrite.java) | diff --git a/google-cloud-firestore/clirr-ignored-differences.xml b/google-cloud-firestore/clirr-ignored-differences.xml index a1064b15c..6685f0d8a 100644 --- a/google-cloud-firestore/clirr-ignored-differences.xml +++ b/google-cloud-firestore/clirr-ignored-differences.xml @@ -346,4 +346,26 @@ void internalStream(*) * + + + + 7012 + com/google/cloud/firestore/Firestore + com.google.cloud.firestore.PipelineSource pipeline() + + + 7013 + com/google/cloud/firestore/Transaction + com.google.api.core.ApiFuture execute(com.google.cloud.firestore.Pipeline) + + + 7013 + com/google/cloud/firestore/Transaction + com.google.api.core.ApiFuture execute(com.google.cloud.firestore.Pipeline, com.google.cloud.firestore.pipeline.stages.PipelineExecuteOptions) + + + 7012 + com/google/cloud/firestore/spi/v1/FirestoreRpc + com.google.api.gax.rpc.ServerStreamingCallable executePipelineCallable() + diff --git a/google-cloud-firestore/pom.xml b/google-cloud-firestore/pom.xml index e2f86c6a7..f00aab94e 100644 --- a/google-cloud-firestore/pom.xml +++ b/google-cloud-firestore/pom.xml @@ -306,6 +306,51 @@ + + maven-assembly-plugin + + + jar-with-dependencies + + + + + make-assembly + package + + single + + + + + + org.apache.maven.plugins + maven-compiler-plugin + + + default-compile + none + + + default-testCompile + none + + + compile + compile + + compile + + + + testCompile + test-compile + + testCompile + + + + diff --git a/google-cloud-firestore/src/main/java/com/google/cloud/firestore/AggregateQuery.java b/google-cloud-firestore/src/main/java/com/google/cloud/firestore/AggregateQuery.java index 89702e423..830d3bc61 100644 --- a/google-cloud-firestore/src/main/java/com/google/cloud/firestore/AggregateQuery.java +++ b/google-cloud-firestore/src/main/java/com/google/cloud/firestore/AggregateQuery.java @@ -16,6 +16,7 @@ package com.google.cloud.firestore; +import static com.google.cloud.firestore.pipeline.expressions.Expression.and; import static com.google.cloud.firestore.telemetry.TelemetryConstants.METHOD_NAME_RUN_AGGREGATION_QUERY; import static com.google.cloud.firestore.telemetry.TraceUtil.ATTRIBUTE_KEY_ATTEMPT; @@ -27,6 +28,8 @@ import com.google.api.gax.rpc.StatusCode; import com.google.api.gax.rpc.StreamController; import com.google.cloud.Timestamp; +import com.google.cloud.firestore.pipeline.expressions.AliasedAggregate; +import com.google.cloud.firestore.pipeline.expressions.BooleanExpression; import com.google.cloud.firestore.telemetry.MetricsUtil.MetricsContext; import com.google.cloud.firestore.telemetry.TelemetryConstants; import com.google.cloud.firestore.telemetry.TelemetryConstants.MetricType; @@ -50,6 +53,7 @@ import java.util.Map; import java.util.Objects; import java.util.Set; +import java.util.stream.Collectors; import javax.annotation.Nonnull; import javax.annotation.Nullable; @@ -84,6 +88,30 @@ public Query getQuery() { return query; } + Pipeline pipeline() { + Pipeline pipeline = getQuery().pipeline(); + + List existsExprs = + this.aggregateFieldList.stream() + .map(PipelineUtils::toPipelineExistsExpr) + .filter(Objects::nonNull) + .collect(Collectors.toList()); + if (existsExprs.size() == 1) { + pipeline = pipeline.where(existsExprs.get(0)); + } else if (existsExprs.size() > 1) { + pipeline = + pipeline.where( + and( + existsExprs.get(0), + existsExprs.subList(1, existsExprs.size()).toArray(new BooleanExpression[0]))); + } + + return pipeline.aggregate( + this.aggregateFieldList.stream() + .map(PipelineUtils::toPipelineAggregatorTarget) + .toArray(AliasedAggregate[]::new)); + } + /** * Executes this query. * diff --git a/google-cloud-firestore/src/main/java/com/google/cloud/firestore/AggregateQuerySnapshot.java b/google-cloud-firestore/src/main/java/com/google/cloud/firestore/AggregateQuerySnapshot.java index ff7b99906..be98e61d2 100644 --- a/google-cloud-firestore/src/main/java/com/google/cloud/firestore/AggregateQuerySnapshot.java +++ b/google-cloud-firestore/src/main/java/com/google/cloud/firestore/AggregateQuerySnapshot.java @@ -16,6 +16,7 @@ package com.google.cloud.firestore; +import com.google.api.core.InternalApi; import com.google.api.core.InternalExtensionOnly; import com.google.cloud.Timestamp; import com.google.firestore.v1.Value; @@ -189,4 +190,9 @@ public boolean equals(Object object) { public int hashCode() { return Objects.hash(query, data); } + + @InternalApi + Map getData() { + return data; + } } diff --git a/google-cloud-firestore/src/main/java/com/google/cloud/firestore/ExplainStats.java b/google-cloud-firestore/src/main/java/com/google/cloud/firestore/ExplainStats.java new file mode 100644 index 000000000..9d255ac73 --- /dev/null +++ b/google-cloud-firestore/src/main/java/com/google/cloud/firestore/ExplainStats.java @@ -0,0 +1,73 @@ +/* + * Copyright 2024 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.api.core.BetaApi; +import com.google.protobuf.Any; +import com.google.protobuf.InvalidProtocolBufferException; +import com.google.protobuf.StringValue; +import javax.annotation.Nonnull; + +/** + * A wrapper object to access explain stats if explain or analyze was enabled for the Pipeline query + * execution. + */ +@BetaApi +public final class ExplainStats { + + private final Any explainStatsData; + + /** + * @hideconstructor + * @param explainStatsData The raw proto message of the explain stats. + */ + ExplainStats(@Nonnull Any explainStatsData) { + this.explainStatsData = explainStatsData; + } + + /** + * Returns the explain stats in an encoded proto format, as returned from the Firestore backend. + * The caller is responsible for unpacking this proto message. + */ + @Nonnull + public Any getRawData() { + return explainStatsData; + } + + private StringValue decode() { + try { + return explainStatsData.unpack(StringValue.class); + } catch (InvalidProtocolBufferException e) { + throw new RuntimeException( + "Unable to decode explain stats. Did you request an output format that returns a string" + + " value, such as 'text' or 'json'?", + e); + } + } + + /** + * When explain stats were requested with `outputFormat = 'text'`, this returns the explain stats + * string verbatim as returned from the Firestore backend. + * + *

If explain stats were requested with `outputFormat = 'json'`, this returns the explain stats + * as stringified JSON, which was returned from the Firestore backend. + */ + @Nonnull + public String getText() { + return decode().getValue(); + } +} diff --git a/google-cloud-firestore/src/main/java/com/google/cloud/firestore/FieldPath.java b/google-cloud-firestore/src/main/java/com/google/cloud/firestore/FieldPath.java index e6939c445..c5b9a8173 100644 --- a/google-cloud-firestore/src/main/java/com/google/cloud/firestore/FieldPath.java +++ b/google-cloud-firestore/src/main/java/com/google/cloud/firestore/FieldPath.java @@ -16,6 +16,7 @@ package com.google.cloud.firestore; +import com.google.api.core.InternalApi; import com.google.auto.value.AutoValue; import com.google.common.base.Preconditions; import com.google.common.collect.ImmutableList; @@ -79,7 +80,8 @@ static boolean isDocumentId(String path) { } /** Returns a field path from a dot separated string. Does not support escaping. */ - static FieldPath fromDotSeparatedString(String field) { + @InternalApi + public static FieldPath fromDotSeparatedString(String field) { if (PROHIBITED_CHARACTERS.matcher(field).matches()) { throw new IllegalArgumentException("Use FieldPath.of() for field names containing '˜*/[]'."); } diff --git a/google-cloud-firestore/src/main/java/com/google/cloud/firestore/Firestore.java b/google-cloud-firestore/src/main/java/com/google/cloud/firestore/Firestore.java index 5bbb1164a..a5a4caad5 100644 --- a/google-cloud-firestore/src/main/java/com/google/cloud/firestore/Firestore.java +++ b/google-cloud-firestore/src/main/java/com/google/cloud/firestore/Firestore.java @@ -66,6 +66,37 @@ public interface Firestore extends Service, AutoCloseable { */ CollectionGroup collectionGroup(@Nonnull String collectionId); + /** + * Creates a new {@link PipelineSource} to build and execute a data pipeline. + * + *

A pipeline is composed of a sequence of stages. Each stage processes the output from the + * previous one, and the final stage's output is the result of the pipeline's execution. + * + *

Example usage: + * + *

{@code
+   * Pipeline pipeline = firestore.pipeline()
+   * .collection("books")
+   * .where(Field("rating").isGreaterThan(4.5))
+   * .sort(Field("rating").descending())
+   * .limit(2);
+   * }
+ * + *

Note on Execution: The stages are conceptual. The Firestore backend may optimize + * execution (e.g., reordering or merging stages) as long as the final result remains the same. + * + *

Important Limitations: + * + *

+ * + * @return A {@code PipelineSource} to begin defining the pipeline's stages. + */ + PipelineSource pipeline(); + /** * Executes the given updateFunction and then attempts to commit the changes applied within the * transaction. If any document read within the transaction has changed, the updateFunction will diff --git a/google-cloud-firestore/src/main/java/com/google/cloud/firestore/FirestoreImpl.java b/google-cloud-firestore/src/main/java/com/google/cloud/firestore/FirestoreImpl.java index 4d532b459..ce0fd6736 100644 --- a/google-cloud-firestore/src/main/java/com/google/cloud/firestore/FirestoreImpl.java +++ b/google-cloud-firestore/src/main/java/com/google/cloud/firestore/FirestoreImpl.java @@ -21,6 +21,7 @@ import com.google.api.core.ApiClock; import com.google.api.core.ApiFuture; +import com.google.api.core.BetaApi; import com.google.api.core.NanoClock; import com.google.api.core.ObsoleteApi; import com.google.api.core.SettableApiFuture; @@ -416,6 +417,13 @@ public CollectionGroup collectionGroup(@Nonnull final String collectionId) { return new CollectionGroup(this, collectionId); } + @Nonnull + @Override + @BetaApi + public PipelineSource pipeline() { + return new PipelineSource(this); + } + @Nonnull @Override public ApiFuture runTransaction(@Nonnull final Transaction.Function updateFunction) { diff --git a/google-cloud-firestore/src/main/java/com/google/cloud/firestore/Pipeline.java b/google-cloud-firestore/src/main/java/com/google/cloud/firestore/Pipeline.java new file mode 100644 index 000000000..8bb00b7b4 --- /dev/null +++ b/google-cloud-firestore/src/main/java/com/google/cloud/firestore/Pipeline.java @@ -0,0 +1,1387 @@ +/* + * Copyright 2024 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.cloud.firestore.pipeline.expressions.Expression.field; +import static com.google.cloud.firestore.telemetry.TraceUtil.ATTRIBUTE_KEY_DOC_COUNT; +import static com.google.cloud.firestore.telemetry.TraceUtil.ATTRIBUTE_KEY_IS_TRANSACTIONAL; + +import com.google.api.core.ApiFuture; +import com.google.api.core.BetaApi; +import com.google.api.core.InternalApi; +import com.google.api.core.InternalExtensionOnly; +import com.google.api.core.SettableApiFuture; +import com.google.api.gax.rpc.ApiStreamObserver; +import com.google.api.gax.rpc.ResponseObserver; +import com.google.api.gax.rpc.StreamController; +import com.google.cloud.Timestamp; +import com.google.cloud.firestore.pipeline.expressions.AggregateFunction; +import com.google.cloud.firestore.pipeline.expressions.AliasedAggregate; +import com.google.cloud.firestore.pipeline.expressions.AliasedExpression; +import com.google.cloud.firestore.pipeline.expressions.BooleanExpression; +import com.google.cloud.firestore.pipeline.expressions.Expression; +import com.google.cloud.firestore.pipeline.expressions.Field; +import com.google.cloud.firestore.pipeline.expressions.FunctionExpression; +import com.google.cloud.firestore.pipeline.expressions.Ordering; +import com.google.cloud.firestore.pipeline.expressions.Selectable; +import com.google.cloud.firestore.pipeline.stages.AddFields; +import com.google.cloud.firestore.pipeline.stages.Aggregate; +import com.google.cloud.firestore.pipeline.stages.AggregateOptions; +import com.google.cloud.firestore.pipeline.stages.Distinct; +import com.google.cloud.firestore.pipeline.stages.FindNearest; +import com.google.cloud.firestore.pipeline.stages.FindNearestOptions; +import com.google.cloud.firestore.pipeline.stages.Limit; +import com.google.cloud.firestore.pipeline.stages.Offset; +import com.google.cloud.firestore.pipeline.stages.PipelineExecuteOptions; +import com.google.cloud.firestore.pipeline.stages.RawStage; +import com.google.cloud.firestore.pipeline.stages.RemoveFields; +import com.google.cloud.firestore.pipeline.stages.ReplaceWith; +import com.google.cloud.firestore.pipeline.stages.Sample; +import com.google.cloud.firestore.pipeline.stages.Select; +import com.google.cloud.firestore.pipeline.stages.Sort; +import com.google.cloud.firestore.pipeline.stages.Stage; +import com.google.cloud.firestore.pipeline.stages.StageUtils; +import com.google.cloud.firestore.pipeline.stages.Union; +import com.google.cloud.firestore.pipeline.stages.Unnest; +import com.google.cloud.firestore.pipeline.stages.UnnestOptions; +import com.google.cloud.firestore.pipeline.stages.Where; +import com.google.cloud.firestore.telemetry.MetricsUtil.MetricsContext; +import com.google.cloud.firestore.telemetry.TelemetryConstants; +import com.google.cloud.firestore.telemetry.TelemetryConstants.MetricType; +import com.google.cloud.firestore.telemetry.TraceUtil; +import com.google.cloud.firestore.telemetry.TraceUtil.Scope; +import com.google.common.collect.FluentIterable; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableMap; +import com.google.firestore.v1.Document; +import com.google.firestore.v1.ExecutePipelineRequest; +import com.google.firestore.v1.ExecutePipelineResponse; +import com.google.firestore.v1.StructuredPipeline; +import com.google.firestore.v1.Value; +import com.google.protobuf.ByteString; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.logging.Level; +import java.util.logging.Logger; +import javax.annotation.Nonnull; +import javax.annotation.Nullable; + +/** + * The Pipeline class provides a flexible and expressive framework for building complex data + * transformation and query pipelines for Firestore. + * + *

A pipeline takes data sources, such as Firestore collections or collection groups, and applies + * a series of stages that are chained together. Each stage takes the output from the previous stage + * (or the data source) and produces an output for the next stage (or as the final output of the + * pipeline). + * + *

Expressions from {@link com.google.cloud.firestore.pipeline.expressions} can be used within + * each stages to filter and transform data through the stage. + * + *

NOTE: The chained stages do not prescribe exactly how Firestore will execute the pipeline. + * Instead, Firestore only guarantees that the result is the same as if the chained stages were + * executed in order. + * + *

Usage Examples: + * + *

{@code
+ * Firestore firestore; // A valid firestore instance.
+ *
+ * // Example 1: Select specific fields and rename 'rating' to 'bookRating'
+ * Snapshot results1 = firestore.pipeline()
+ *     .collection("books")
+ *     .select(field("title"), field("author"), field("rating").as("bookRating"))
+ *     .execute()
+ *     .get();
+ *
+ * // Example 2: Filter documents where 'genre' is "Science Fiction" and 'published' is after 1950
+ * Snapshot results2 = firestore.pipeline()
+ *     .collection("books")
+ *     .where(and(eq("genre", "Science Fiction"), gt("published", 1950)))
+ *     .execute()
+ *     .get();
+ * // Same as above but using methods on expressions as opposed to static functions.
+ * results2 = firestore.pipeline()
+ *     .collection("books")
+ *     .where(and(field("genre").eq("Science Fiction"), field("published").gt(1950)))
+ *     .execute()
+ *     .get();
+ *
+ * // Example 3: Calculate the average rating of books published after 1980
+ * Snapshot results3 = firestore.pipeline()
+ *     .collection("books")
+ *     .where(gt("published", 1980))
+ *     .aggregate(avg("rating").as("averageRating"))
+ *     .execute()
+ *     .get();
+ * }
+ */ +@BetaApi +public final class Pipeline { + /** + * A Snapshot contains the results of a pipeline execution. It can be used to access the + * documents, execution time, and explain stats. + */ + @BetaApi + public static final class Snapshot { + + private final Pipeline pipeline; + private final Timestamp executionTime; + private final List results; + private final ExplainStats explainStats; + + Snapshot( + @Nonnull Pipeline pipeline, + @Nonnull List results, + @Nonnull Timestamp executionTime, + @Nullable ExplainStats explainStats) { + this.pipeline = pipeline; + this.results = results; + this.executionTime = executionTime; + this.explainStats = explainStats; + } + + /** + * The Pipeline on which you called `execute()` in order to get this `Snapshot`. + * + * @return The pipeline that was executed. + */ + @Nonnull + Pipeline getPipeline() { + return pipeline; + } + + /** + * An array of all the results in the `Snapshot`. + * + * @return The list of results. + */ + @Nonnull + public List getResults() { + return results; + } + + /** + * The time at which the pipeline producing this result is executed. + * + * @return The execution time of the pipeline. + */ + @Nonnull + public Timestamp getExecutionTime() { + return executionTime; + } + + /** + * Return stats from query explain. + * + *

If `explainOptions.mode` was set to `execute` or left unset, then this returns `null`. + * + * @return The explain stats, or `null` if not available. + */ + @Nullable + public ExplainStats getExplainStats() { + return explainStats; + } + } + + private static Logger logger = Logger.getLogger(Pipeline.class.getName()); + private final FluentIterable stages; + private final FirestoreRpcContext rpcContext; + + private Pipeline(FirestoreRpcContext rpcContext, FluentIterable stages) { + this.rpcContext = rpcContext; + this.stages = stages; + } + + @InternalApi + Pipeline(FirestoreRpcContext rpcContext, Stage stage) { + this(rpcContext, FluentIterable.of(stage)); + } + + private Pipeline append(Stage stage) { + return new Pipeline(this.rpcContext, stages.append(stage)); + } + + /** + * Adds new fields to outputs from previous stages. + * + *

This stage allows you to compute values on-the-fly based on existing data from previous + * stages or constants. You can use this to create new fields or overwrite existing ones (if there + * is name overlaps). + * + *

The added fields are defined using {@link Selectable} expressions, which can be: + * + *

    + *
  • {@link Field}: References an existing document field. + *
  • {@link FunctionExpression}: Performs a calculation using functions like `add`, `multiply` + * with assigned aliases using {@link Expression#as(String)}. + *
+ * + *

Example: + * + *

{@code
+   * firestore.pipeline().collection("books")
+   *   .addFields(
+   *     field("rating").as("bookRating"), // Rename 'rating' to 'bookRating'
+   *     add(5, field("quantity")).as("totalCost")  // Calculate 'totalCost'
+   *   );
+   * }
+ * + * @param fields The fields to add to the documents, specified as {@link Selectable} expressions. + * @return A new Pipeline object with this stage appended to the stage list. + */ + @BetaApi + public Pipeline addFields(Selectable... fields) { + return append(new AddFields(PipelineUtils.selectablesToMap(fields))); + } + + /** + * Remove fields from outputs of previous stages. + * + *

Example: + * + *

{@code
+   * firestore.pipeline().collection("books")
+   *   .removeFields(
+   *     "rating", "cost"
+   *   );
+   * }
+ * + * @param fields The fields to remove. + * @return A new Pipeline object with this stage appended to the stage list. + */ + @BetaApi + public Pipeline removeFields(String... fields) { + return append( + new RemoveFields( + ImmutableList.builder() + .addAll(Arrays.stream(fields).map(f -> Field.ofUserPath(f)).iterator()) + .build())); + } + + /** + * Remove fields from outputs of previous stages. + * + *

Example: + * + *

{@code
+   * firestore.pipeline().collection("books")
+   *   .removeFields(
+   *     field("rating"), field("cost")
+   *   );
+   * }
+ * + * @param fields The fields to remove. + * @return A new Pipeline object with this stage appended to the stage list. + */ + @BetaApi + public Pipeline removeFields(Field... fields) { + return append( + new RemoveFields( + ImmutableList.builder().addAll(Arrays.stream(fields).iterator()).build())); + } + + /** + * Selects or creates a set of fields from the outputs of previous stages. + * + *

The selected fields are defined using {@link Selectable} expressions, which can be: + * + *

    + *
  • {@link Field}: References an existing document field. + *
  • {@link FunctionExpression}: Represents the result of a function with an assigned alias + * name using {@link Expression#as(String)} + *
+ * + *

If no selections are provided, the output of this stage is empty. Use {@link + * com.google.cloud.firestore.Pipeline#addFields(Selectable...)} instead if only additions are + * desired. + * + *

Example: + * + *

{@code
+   * firestore.pipeline().collection("books")
+   *   .select(
+   *     field("name"),
+   *     field("address").toUppercase().as("upperAddress"),
+   *   );
+   * }
+ * + * @param selections The fields to include in the output documents, specified as {@link + * Selectable} expressions. + * @return A new Pipeline object with this stage appended to the stage list. + */ + @BetaApi + public Pipeline select(Selectable... selections) { + return append(new Select(PipelineUtils.selectablesToMap(selections))); + } + + /** + * Selects a set of fields from the outputs of previous stages. + * + *

If no selections are provided, the output of this stage is empty. Use {@link + * com.google.cloud.firestore.Pipeline#addFields(Selectable...)} instead if only additions are + * desired. + * + *

Example: + * + *

{@code
+   * firestore.collection("books")
+   *   .select("name", "address");
+   *
+   * // The above is a shorthand of this:
+   * firestore.pipeline().collection("books")
+   *    .select(field("name"), field("address"));
+   * }
+ * + * @param fields The name of the fields to include in the output documents. + * @return A new Pipeline object with this stage appended to the stage list. + */ + @BetaApi + public Pipeline select(String... fields) { + return append(new Select(PipelineUtils.fieldNamesToMap(fields))); + } + + /** + * Filters the documents from previous stages to only include those matching the specified {@link + * BooleanExpression}. + * + *

This stage allows you to apply conditions to the data, similar to a "WHERE" clause in SQL. + * You can filter documents based on their field values, using implementions of {@link + * BooleanExpression}, typically including but not limited to: + * + *

    + *
  • field comparators: {@link FunctionExpression#equal}, {@link FunctionExpression#lessThan} + * (less than), {@link FunctionExpression#greaterThan} (greater than), etc. + *
  • logical operators: {@link FunctionExpression#and}, {@link FunctionExpression#or}, {@link + * FunctionExpression#not}, etc. + *
  • advanced functions: {@link FunctionExpression#regexMatch(String, String)}, {@link + * FunctionExpression#arrayContains(Expression, Expression)}, etc. + *
+ * + *

Example: + * + *

{@code
+   * firestore.pipeline().collection("books")
+   *   .where(
+   *     and(
+   *         gt("rating", 4.0),   // Filter for ratings greater than 4.0
+   *         field("genre").eq("Science Fiction") // Equivalent to eq("genre", "Science Fiction")
+   *     )
+   *   );
+   * }
+ * + * @param condition The {@link BooleanExpression} to apply. + * @return A new Pipeline object with this stage appended to the stage list. + */ + @BetaApi + public Pipeline where(BooleanExpression condition) { + return append(new Where(condition)); + } + + /** + * Skips the first `offset` number of documents from the results of previous stages. + * + *

This stage is useful for implementing pagination in your pipelines, allowing you to retrieve + * results in chunks. It is typically used in conjunction with {@link #limit(int)} to control the + * size of each page. + * + *

Example: + * + *

{@code
+   * // Retrieve the second page of 20 results
+   * firestore.pipeline().collection("books")
+   *     .sort(field("published").descending())
+   *     .offset(20)  // Skip the first 20 results
+   *     .limit(20);   // Take the next 20 results
+   * }
+ * + * @param offset The number of documents to skip. + * @return A new Pipeline object with this stage appended to the stage list. + */ + @BetaApi + public Pipeline offset(int offset) { + return append(new Offset(offset)); + } + + /** + * Limits the maximum number of documents returned by previous stages to `limit`. + * + *

This stage is particularly useful when you want to retrieve a controlled subset of data from + * a potentially large result set. It's often used for: + * + *

    + *
  • **Pagination:** In combination with {@link #offset(int)} to retrieve specific pages of + * results. + *
  • **Limiting Data Retrieval:** To prevent excessive data transfer and improve performance, + * especially when dealing with large collections. + *
+ * + *

Example: + * + *

{@code
+   * // Limit the results to the top 10 highest-rated books
+   * firestore.pipeline().collection("books")
+   *     .sort(field("rating").descending())
+   *     .limit(10);
+   * }
+ * + * @param limit The maximum number of documents to return. + * @return A new Pipeline object with this stage appended to the stage list. + */ + @BetaApi + public Pipeline limit(int limit) { + return append(new Limit(limit)); + } + + /** + * Performs aggregation operations on the documents from previous stages. + * + *

This stage allows you to calculate aggregate values over a set of documents. You define the + * aggregations to perform using {@link AliasedExpression} expressions which are typically results + * of calling {@link Expression#as(String)} on {@link AggregateFunction} instances. + * + *

Example: + * + *

{@code
+   * // Calculate the average rating and the total number of books
+   * firestore.pipeline().collection("books")
+   *     .aggregate(
+   *         field("rating").avg().as("averageRating"),
+   *         countAll().as("totalBooks")
+   *     );
+   * }
+ * + * @param accumulators The {@link AliasedExpression} expressions, each wrapping an {@link + * AggregateFunction} and provide a name for the accumulated results. + * @return A new Pipeline object with this stage appended to the stage list. + */ + @BetaApi + public Pipeline aggregate(AliasedAggregate... accumulators) { + return append(Aggregate.withAccumulators(accumulators)); + } + + /** + * Performs optionally grouped aggregation operations on the documents from previous stages. + * + *

This stage allows you to calculate aggregate values over a set of documents, optionally + * grouped by one or more fields or functions. You can specify: + * + *

    + *
  • **Grouping Fields or Functions:** One or more fields or functions to group the documents + * by. For each distinct combination of values in these fields, a separate group is created. + * If no grouping fields are provided, a single group containing all documents is used. Not + * specifying groups is the same as putting the entire inputs into one group. + *
  • **Accumulators:** One or more accumulation operations to perform within each group. These + * are defined using {@link AliasedExpression} expressions, which are typically created by + * calling {@link Expression#as(String)} on {@link AggregateFunction} instances. Each + * aggregation calculates a value (e.g., sum, average, count) based on the documents within + * its group. + *
+ * + *

Example: + * + *

{@code
+   * // Calculate the average rating for each genre.
+   * firestore.pipeline().collection("books")
+   *   .aggregate(
+   *     Aggregate
+   *       .withAccumulators(avg("rating").as("avg_rating"))
+   *       .withGroups("genre"));
+   * }
+ * + * @param aggregate An {@link Aggregate} object that specifies the grouping fields (if any) and + * the aggregation operations to perform. + * @return A new {@code Pipeline} object with this stage appended to the stage list. + */ + @BetaApi + public Pipeline aggregate(Aggregate aggregate) { + return append(aggregate); + } + + @BetaApi + public Pipeline aggregate(Aggregate aggregate, AggregateOptions options) { + return append(aggregate.withOptions(options)); + } + + /** + * Returns a set of distinct field values from the inputs to this stage. + * + *

This stage run through the results from previous stages to include only results with unique + * combinations of values for the specified fields and produce these fields as the output. + * + *

Example: + * + *

{@code
+   * // Get a list of unique genres.
+   * firestore.pipeline().collection("books")
+   *     .distinct("genre");
+   * }
+ * + * @param fields The fields to consider when determining distinct values. + * @return A new {@code Pipeline} object with this stage appended to the stage list. + */ + @BetaApi + public Pipeline distinct(String... fields) { + return append(new Distinct(PipelineUtils.fieldNamesToMap(fields))); + } + + /** + * Returns a set of distinct {@link Expression} values from the inputs to this stage. + * + *

This stage run through the results from previous stages to include only results with unique + * combinations of {@link Expression} values ({@link Field}, {@link FunctionExpression}, etc). + * + *

The parameters to this stage are defined using {@link Selectable} expressions, which can be: + * + *

    + *
  • {@link Field}: References an existing document field. + *
  • {@link FunctionExpression}: Represents the result of a function with an assigned alias + * name using {@link Expression#as(String)} + *
+ * + *

Example: + * + *

{@code
+   * // Get a list of unique author names in uppercase and genre combinations.
+   * firestore.pipeline().collection("books")
+   *     .distinct(toUppercase(field("author")).as("authorName"), field("genre"))
+   *     .select("authorName");
+   * }
+ * + * @param selectables The {@link Selectable} expressions to consider when determining distinct + * value combinations. + * @return A new {@code Pipeline} object with this stage appended to the stage list. + */ + @BetaApi + public Pipeline distinct(Selectable... selectables) { + return append(new Distinct(PipelineUtils.selectablesToMap(selectables))); + } + + /** + * Performs vector distance (similarity) search with given parameters to the stage inputs. + * + *

This stage adds a "nearest neighbor search" capability to your pipelines. Given a field that + * stores vectors and a target vector, this stage will identify and return the inputs whose vector + * field is closest to the target vector, using the parameters specified in `options`. + * + *

Example: + * + *

{@code
+   * // Find books with similar "topicVectors" to the given targetVector
+   * firestore.pipeline().collection("books")
+   *     .findNearest("topicVectors", targetVector, FindNearest.DistanceMeasure.COSINE,
+   *        new FindNearestOptions()
+   *          .withLimit(10)
+   *          .withDistanceField("distance"));
+   * }
+ * + * @param fieldName The name of the field containing the vector data. This field should store + * {@link VectorValue}. + * @param vector The target vector to compare against. + * @param distanceMeasure The distance measure to use: cosine, euclidean, etc. + * @param options Configuration options for the nearest neighbor search, such as limit and output + * distance field name. + * @return A new {@code Pipeline} object with this stage appended to the stage list. + */ + @BetaApi + public Pipeline findNearest( + String fieldName, + double[] vector, + FindNearest.DistanceMeasure distanceMeasure, + FindNearestOptions options) { + return findNearest(field(fieldName), vector, distanceMeasure, options); + } + + /** + * Performs vector distance (similarity) search with given parameters to the stage inputs. + * + *

This stage adds a "nearest neighbor search" capability to your pipelines. Given an + * expression that evaluates to a vector and a target vector, this stage will identify and return + * the inputs whose vector expression is closest to the target vector, using the parameters + * specified in `options`. + * + *

Example: + * + *

{@code
+   * // Find books with similar "topicVectors" to the given targetVector
+   * firestore.pipeline().collection("books")
+   *     .findNearest(
+   *        field("topicVectors"),
+   *        targetVector,
+   *        FindNearest.DistanceMeasure.COSINE,
+   *        new FindNearestOptions()
+   *          .withLimit(10)
+   *          .withDistanceField("distance"));
+   * }
+ * + * @param property The expression that evaluates to a vector value using the stage inputs. + * @param vector The target vector to compare against. + * @param distanceMeasure The distance measure to use: cosine, euclidean, etc. + * @param options Configuration options for the nearest neighbor search, such as limit and output + * distance field name. + * @return A new {@code Pipeline} object with this stage appended to the stage list. + */ + @BetaApi + public Pipeline findNearest( + Expression property, + double[] vector, + FindNearest.DistanceMeasure distanceMeasure, + FindNearestOptions options) { + // Implementation for findNearest (add the FindNearest stage if needed) + return append(new FindNearest(property, new VectorValue(vector), distanceMeasure, options)); + } + + /** + * Sorts the documents from previous stages based on one or more {@link Ordering} criteria. + * + *

This stage allows you to order the results of your pipeline. You can specify multiple {@link + * Ordering} instances to sort by multiple fields in ascending or descending order. If documents + * have the same value for a field used for sorting, the next specified ordering will be used. If + * all orderings result in equal comparison, the documents are considered equal and the order is + * unspecified. + * + *

Example: + * + *

{@code
+   * // Sort books by rating in descending order, and then by title in ascending order for books with the same rating
+   * firestore.pipeline().collection("books")
+   *     .sort(
+   *         Ordering.of("rating").descending(),
+   *         Ordering.of("title")  // Ascending order is the default
+   *     );
+   * }
+ * + * @param orders One or more {@link Ordering} instances specifying the sorting criteria. + * @return A new {@code Pipeline} object with this stage appended to the stage list. + */ + @BetaApi + public Pipeline sort(Ordering... orders) { + return append(new Sort(ImmutableList.copyOf(orders))); + } + + /** + * Fully overwrites all fields in a document with those coming from a nested map. + * + *

This stage allows you to emit a map value as a document. Each key of the map becomes a field + * on the document that contains the corresponding value. + * + *

Example: + * + *

{@code
+   * // Input.
+   * // {
+   * //  "name": "John Doe Jr.",
+   * //  "parents": {
+   * //    "father": "John Doe Sr.",
+   * //    "mother": "Jane Doe"
+   * //   }
+   * // }
+   *
+   * // Emit parents as document.
+   * firestore.pipeline().collection("people").replaceWith("parents");
+   *
+   * // Output
+   * // {
+   * //  "father": "John Doe Sr.",
+   * //  "mother": "Jane Doe"
+   * // }
+   * }
+ * + * @param fieldName The name of the field containing the nested map. + * @return A new {@code Pipeline} object with this stage appended to the stage list. + */ + @BetaApi + public Pipeline replaceWith(String fieldName) { + return replaceWith(field(fieldName)); + } + + /** + * Fully overwrites all fields in a document with those coming from a nested map. + * + *

This stage allows you to emit a map value as a document. Each key of the map becomes a field + * on the document that contains the corresponding value. + * + *

Example: + * + *

{@code
+   * // Input.
+   * // {
+   * //  "name": "John Doe Jr.",
+   * //  "parents": {
+   * //    "father": "John Doe Sr.",
+   * //    "mother": "Jane Doe"
+   * //  }
+   * // }
+   *
+   * // Emit parents as document.
+   * firestore.pipeline().collection("people").replaceWith(field("parents"));
+   *
+   * // Output
+   * // {
+   * //  "father": "John Doe Sr.",
+   * //  "mother": "Jane Doe"
+   * // }
+   * }
+ * + * @param expr The {@link Expression} field containing the nested map. + * @return A new {@code Pipeline} object with this stage appended to the stage list. + */ + @BetaApi + public Pipeline replaceWith(Expression expr) { + return append(new ReplaceWith(expr)); + } + + /** + * Performs a pseudo-random sampling of the documents from the previous stage. + * + *

This stage will filter documents pseudo-randomly. The 'limit' parameter specifies the number + * of documents to emit from this stage, but if there are fewer documents from previous stage than + * the 'limit' parameter, then no filtering will occur and all documents will pass through. + * + *

Example: + * + *

{@code
+   * // Sample 10 books, if available.
+   * firestore.pipeline().collection("books")
+   *     .sample(10);
+   * }
+ * + * @param limit The number of documents to emit, if possible. + * @return A new {@code Pipeline} object with this stage appended to the stage list. + */ + @BetaApi + public Pipeline sample(int limit) { + return sample(Sample.withDocLimit(limit)); + } + + /** + * Performs a pseudo-random sampling of the documents from the previous stage. + * + *

This stage will filter documents pseudo-randomly. The 'options' parameter specifies how + * sampling will be performed. See {@code SampleOptions} for more information. + * + *

Examples: + * + *

{@code
+   * // Sample 10 books, if available.
+   * firestore.pipeline().collection("books")
+   *     .sample(Sample.withDocLimit(10));
+   *
+   * // Sample 50% of books.
+   * firestore.pipeline().collection("books")
+   *     .sample(Sample.withPercentage(0.5));
+   * }
+ * + * @param sample The {@code Sample} specifies how sampling is performed. + * @return A new {@code Pipeline} object with this stage appended to the stage list. + */ + @BetaApi + public Pipeline sample(Sample sample) { + return append(sample); + } + + /** + * Performs union of all documents from two pipelines, including duplicates. + * + *

This stage will pass through documents from previous stage, and also pass through documents + * from previous stage of the `other` {@code Pipeline} given in parameter. The order of documents + * emitted from this stage is undefined. + * + *

Example: + * + *

{@code
+   * // Emit documents from books collection and magazines collection.
+   * firestore.pipeline().collection("books")
+   *     .union(firestore.pipeline().collection("magazines"));
+   * }
+ * + * @param other The other {@code Pipeline} that is part of union. + * @return A new {@code Pipeline} object with this stage appended to the stage list. + */ + @BetaApi + public Pipeline union(Pipeline other) { + return append(new Union(other)); + } + + /** + * Produces a document for each element in array found in previous stage document. + * + *

For each previous stage document, this stage will emit zero or more augmented documents. The + * input array found in the previous stage document field specified by the `fieldName` parameter, + * will for each input array element produce an augmented document. The input array element will + * augment the previous stage document by replacing the field specified by `fieldName` parameter + * with the element value. + * + *

In other words, the field containing the input array will be removed from the augmented + * document and replaced by the corresponding array element. + * + *

Example: + * + *

{@code
+   * // Input:
+   * // { "title": "The Hitchhiker's Guide to the Galaxy", "tags": [ "comedy", "space", "adventure" ], ... }
+   *
+   * // Emit a book document for each tag of the book.
+   * firestore.pipeline().collection("books")
+   *     .unnest("tags", "tag");
+   *
+   * // Output:
+   * // { "title": "The Hitchhiker's Guide to the Galaxy", "tag": "comedy", ... }
+   * // { "title": "The Hitchhiker's Guide to the Galaxy", "tag": "space", ... }
+   * // { "title": "The Hitchhiker's Guide to the Galaxy", "tag": "adventure", ... }
+   * }
+ * + * @param fieldName The name of the field containing the array. + * @return A new {@code Pipeline} object with this stage appended to the stage list. + */ + @BetaApi + public Pipeline unnest(String fieldName, String alias) { + // return unnest(field(fieldName)); + return append(new Unnest(field(fieldName), alias)); + } + + // /** + // * Produces a document for each element in array found in previous stage document. + // * + // *

For each previous stage document, this stage will emit zero or more augmented documents. + // * The input array found in the specified by {@code Selectable} expression parameter, will for + // * each input array element produce an augmented document. The input array element will augment + // * the previous stage document by assigning the {@code Selectable} alias the element value. + // * + // *

Example: + // * + // *

{@code
+  //  * // Input:
+  //  * // { "title": "The Hitchhiker's Guide to the Galaxy", "tags": [ "comedy", "space",
+  // "adventure" ], ... }
+  //  *
+  //  * // Emit a book document for each tag of the book.
+  //  * firestore.pipeline().collection("books")
+  //  *     .unnest(field("tags").as("tag"));
+  //  *
+  //  * // Output:
+  //  * // { "title": "The Hitchhiker's Guide to the Galaxy", "tag": "comedy", "tags": [ "comedy",
+  // "space", "adventure" ], ... }
+  //  * // { "title": "The Hitchhiker's Guide to the Galaxy", "tag": "space", "tags": [ "comedy",
+  // "space", "adventure" ], ... }
+  //  * // { "title": "The Hitchhiker's Guide to the Galaxy", "tag": "adventure", "tags": [
+  // "comedy", "space", "adventure" ], ... }
+  //  * }
+ // * + // * @param field The expression that evaluates to the input array. + // * @return A new {@code Pipeline} object with this stage appended to the stage list. + // */ + // @BetaApi + // public Pipeline unnest(Selectable field) { + // return append(new Unnest(field)); + // } + + /** + * Produces a document for each element in array found in previous stage document. + * + *

For each previous stage document, this stage will emit zero or more augmented documents. The + * input array found in the previous stage document field specified by the `fieldName` parameter, + * will for each input array element produce an augmented document. The input array element will + * augment the previous stage document by replacing the field specified by `fieldName` parameter + * with the element value. + * + *

In other words, the field containing the input array will be removed from the augmented + * document and replaced by the corresponding array element. + * + *

Example: + * + *

{@code
+   * // Input:
+   * // { "title": "The Hitchhiker's Guide to the Galaxy", "tags": [ "comedy", "space", "adventure" ], ... }
+   *
+   * // Emit a book document for each tag of the book.
+   * firestore.pipeline().collection("books")
+   *     .unnest("tags", "tag", new UnnestOptions().withIndexField("tagIndex"));
+   *
+   * // Output:
+   * // { "title": "The Hitchhiker's Guide to the Galaxy", "tagIndex": 0, "tag": "comedy", ... }
+   * // { "title": "The Hitchhiker's Guide to the Galaxy", "tagIndex": 1, "tag": "space", ... }
+   * // { "title": "The Hitchhiker's Guide to the Galaxy", "tagIndex": 2, "tag": "adventure", ... }
+   * }
+ * + * @param fieldName The name of the field containing the array. + * @param options The {@code UnnestOptions} options. + * @return A new {@code Pipeline} object with this stage appended to the stage list. + */ + @BetaApi + public Pipeline unnest(String fieldName, String alias, UnnestOptions options) { + return append(new Unnest(field(fieldName), alias, options)); + } + + /** + * Produces a document for each element in array found in previous stage document. + * + *

For each previous stage document, this stage will emit zero or more augmented documents. The + * input array found in the previous stage document field specified by the `fieldName` parameter, + * will for each input array element produce an augmented document. The input array element will + * augment the previous stage document by replacing the field specified by `fieldName` parameter + * with the element value. + * + *

In other words, the field containing the input array will be removed from the augmented + * document and replaced by the corresponding array element. + * + *

Example: + * + *

{@code
+   * // Input:
+   * // { "title": "The Hitchhiker's Guide to the Galaxy", "tags": [ "comedy", "space", "adventure" ], ... }
+   *
+   * // Emit a book document for each tag of the book.
+   * firestore.pipeline().collection("books")
+   *     .unnest(field("tags").as("tag"));
+   *
+   * // Output:
+   * // { "title": "The Hitchhiker's Guide to the Galaxy", "tagIndex": 0, "tag": "comedy", ... }
+   * // { "title": "The Hitchhiker's Guide to the Galaxy", "tagIndex": 1, "tag": "space", ... }
+   * // { "title": "The Hitchhiker's Guide to the Galaxy", "tagIndex": 2, "tag": "adventure", ... }
+   * }
+ * + * @param expr The name of the expression containing the array. + * @return A new {@code Pipeline} object with this stage appended to the stage list. + */ + @BetaApi + public Pipeline unnest(Selectable expr) { + return append(new Unnest(expr)); + } + + // /** + // * Produces a document for each element in array found in previous stage document. + // * + // *

For each previous stage document, this stage will emit zero or more augmented documents. + // * The input array found in the specified by {@code Selectable} expression parameter, will for + // * each input array element produce an augmented document. The input array element will augment + // * the previous stage document by assigning the {@code Selectable} alias the element value. + // * + // *

Example: + // * + // *

{@code
+  //  * // Input:
+  //  * // { "title": "The Hitchhiker's Guide to the Galaxy", "tags": [ "comedy", "space",
+  // "adventure" ], ... }
+  //  *
+  //  * // Emit a book document for each tag of the book.
+  //  * firestore.pipeline().collection("books")
+  //  *     .unnest(field("tags").as("tag"), UnnestOptions.indexField("tagIndex"));
+  //  *
+  //  * // Output:
+  //  * // { "title": "The Hitchhiker's Guide to the Galaxy", "tagIndex": 0, "tag": "comedy",
+  // "tags": [ "comedy", "space", "adventure" ], ... }
+  //  * // { "title": "The Hitchhiker's Guide to the Galaxy", "tagIndex": 1, "tag": "space", "tags":
+  // [ "comedy", "space", "adventure" ], ... }
+  //  * // { "title": "The Hitchhiker's Guide to the Galaxy", "tagIndex": 2, "tag": "adventure",
+  // "tags": [ "comedy", "space", "adventure" ], ... }
+  //  * }
+ // * + // * @param field The expression that evaluates to the input array. + // * @param options The {@code UnnestOptions} options. + // * @return A new {@code Pipeline} object with this stage appended to the stage list. + // */ + // @BetaApi + // public Pipeline unnest(Selectable field, UnnestOptions options) { + // return append(new Unnest(field, options)); + // } + + /** + * Adds a generic stage to the pipeline. + * + *

This method provides a flexible way to extend the pipeline's functionality by adding custom + * stages. Each generic stage is defined by a unique `name` and a set of `params` that control its + * behavior. + * + *

Example (Assuming there is no "where" stage available in SDK): + * + *

{@code
+   * // Assume we don't have a built-in "where" stage
+   * Map whereParams = new HashMap<>();
+   * whereParams.put("condition", field("published").lt(1900));
+   *
+   * firestore.pipeline().collection("books")
+   *     .genericStage("where", Lists.newArrayList(field("published").lt(1900)), new RawOptions()) // Custom "where" stage
+   *     .select("title", "author");
+   * }
+ * + * @return A new {@code Pipeline} object with this stage appended to the stage list. + */ + @BetaApi + public Pipeline rawStage(RawStage stage) { + return append(stage); + } + + /** + * Executes this pipeline and returns a future to represent the asynchronous operation. + * + *

The returned {@link ApiFuture} can be used to track the progress of the pipeline execution + * and retrieve the results (or handle any errors) asynchronously. + * + *

The pipeline results are returned as a list of {@link PipelineResult} objects. Each {@link + * PipelineResult} typically represents a single key/value map that has passed through all the + * stages of the pipeline, however this might differ depends on the stages involved in the + * pipeline. For example: + * + *

    + *
  • If there are no stages or only transformation stages, each {@link PipelineResult} + * represents a single document. + *
  • If there is an aggregation, only a single {@link PipelineResult} is returned, + * representing the aggregated results over the entire dataset . + *
  • If there is an aggregation stage with grouping, each {@link PipelineResult} represents a + * distinct group and its associated aggregated values. + *
+ * + *

Example: + * + *

{@code
+   * ApiFuture futureResults = firestore.pipeline().collection("books")
+   *     .where(gt("rating", 4.5))
+   *     .select("title", "author", "rating")
+   *     .execute();
+   * }
+ * + * @return An {@link ApiFuture} representing the asynchronous pipeline execution. + */ + @BetaApi + public ApiFuture execute() { + return execute(new PipelineExecuteOptions(), null, null); + } + + @BetaApi + public ApiFuture execute(PipelineExecuteOptions options) { + return execute(options, null, null); + } + + MetricsContext createMetricsContext(String methodName) { + return rpcContext.getFirestore().getOptions().getMetricsUtil().createMetricsContext(methodName); + } + + /** + * Executes this pipeline, providing results to the given {@link ApiStreamObserver} as they become + * available. + * + *

This method allows you to process pipeline results in a streaming fashion, rather than + * waiting for the entire pipeline execution to complete. The provided {@link ApiStreamObserver} + * will receive: + * + *

    + *
  • **onNext(PipelineResult):** Called for each {@link PipelineResult} produced by the + * pipeline. Each {@link PipelineResult} typically represents a single key/value map that + * has passed through all the stages. However, the exact structure might differ based on the + * stages involved in the pipeline (as described in {@link #execute()}). + *
  • **onError(Throwable):** Called if an error occurs during pipeline execution. + *
  • **onCompleted():** Called when the pipeline has finished processing all documents. + *
+ * + *

Example: + * + *

{@code
+   * firestore.pipeline().collection("books")
+   *     .where(gt("rating", 4.5))
+   *     .select("title", "author", "rating")
+   *     .execute(new ApiStreamObserver() {
+   *         @Override
+   *         public void onNext(PipelineResult result) {
+   *             // Process each result as it arrives
+   *             System.out.println(result.getData());
+   *         }
+   *
+   *         @Override
+   *         public void onError(Throwable t) {
+   *             // Handle errors during execution
+   *             t.printStackTrace();
+   *         }
+   *
+   *         @Override
+   *         public void onCompleted() {
+   *             System.out.println("Pipeline execution completed.");
+   *         }
+   *     });
+   * }
+ * + * @param observer The {@link ApiStreamObserver} to receive pipeline results and events. + */ + @BetaApi + public void execute(ApiStreamObserver observer) { + MetricsContext metricsContext = + createMetricsContext(TelemetryConstants.METHOD_NAME_EXECUTE_PIPELINE_EXECUTE); + + executeInternal( + new PipelineExecuteOptions(), + null, + null, + new PipelineResultObserver() { + @Override + public void onNext(PipelineResult result) { + observer.onNext(result); + } + + @Override + public void onError(Throwable t) { + observer.onError(t); + } + + @Override + public void onCompleted() { + observer.onCompleted(); + } + }, + metricsContext); + } + + ApiFuture execute( + @Nonnull PipelineExecuteOptions options, + @Nullable final ByteString transactionId, + @Nullable com.google.protobuf.Timestamp readTime) { + TraceUtil.Span span = + rpcContext + .getFirestore() + .getOptions() + .getTraceUtil() + .startSpan(TelemetryConstants.METHOD_NAME_PIPELINE_EXECUTE); + + MetricsContext metricsContext = + createMetricsContext(TelemetryConstants.METHOD_NAME_EXECUTE_PIPELINE_EXECUTE); + + try (Scope ignored = span.makeCurrent()) { + SettableApiFuture futureResult = SettableApiFuture.create(); + + executeInternal( + options, + transactionId, + readTime, + new PipelineResultObserver() { + final List results = new ArrayList<>(); + + @Override + public void onCompleted() { + futureResult.set( + new Snapshot(Pipeline.this, results, getExecutionTime(), getExplainStats())); + } + + @Override + public void onNext(PipelineResult result) { + results.add(result); + } + + @Override + public void onError(Throwable t) { + futureResult.setException(t); + } + }, + metricsContext); + + span.endAtFuture(futureResult); + return futureResult; + } catch (Exception error) { + span.end(error); + metricsContext.recordLatency(MetricType.END_TO_END_LATENCY, error); + throw error; + } + } + + void executeInternal( + @Nonnull PipelineExecuteOptions options, + @Nullable final ByteString transactionId, + @Nullable com.google.protobuf.Timestamp readTime, + PipelineResultObserver observer, + MetricsContext metricsContext) { + ExecutePipelineRequest.Builder request = + ExecutePipelineRequest.newBuilder() + .setDatabase(rpcContext.getDatabaseName()) + .setStructuredPipeline( + StructuredPipeline.newBuilder() + .setPipeline(toProto()) + .putAllOptions(StageUtils.toMap(options)) + .build()); + + if (transactionId != null) { + request.setTransaction(transactionId); + } + + if (readTime != null) { + request.setReadTime(readTime); + } + + pipelineInternalStream( + request.build(), + new PipelineResultObserver() { + @Override + public void onCompleted() { + observer.setExplainStats(getExplainStats()); + observer.setExecutionTime(getExecutionTime()); + observer.onCompleted(); + } + + @Override + public void onNext(PipelineResult result) { + observer.onNext(result); + } + + @Override + public void onError(Throwable t) { + observer.onError(t); + } + }, + metricsContext); + } + + @InternalApi + private com.google.firestore.v1.Pipeline toProto() { + return com.google.firestore.v1.Pipeline.newBuilder() + .addAllStages(stages.transform(StageUtils::toStageProto)) + .build(); + } + + @InternalApi + public com.google.firestore.v1.Value toProtoValue() { + return Value.newBuilder().setPipelineValue(toProto()).build(); + } + + private void pipelineInternalStream( + ExecutePipelineRequest request, + PipelineResultObserver resultObserver, + MetricsContext metricsContext) { + TraceUtil traceUtil = rpcContext.getFirestore().getOptions().getTraceUtil(); + + // To reduce the size of traces, we only register one event for every 100 responses + // that we receive from the server. + final int NUM_RESPONSES_PER_TRACE_EVENT = 100; + + TraceUtil.Span currentSpan = traceUtil.currentSpan(); + currentSpan.addEvent( + TelemetryConstants.METHOD_NAME_EXECUTE_PIPELINE, + new ImmutableMap.Builder() + .put(ATTRIBUTE_KEY_IS_TRANSACTIONAL, request.hasTransaction()) + .build()); + + ResponseObserver observer = + new ResponseObserver() { + Timestamp executionTime = null; + boolean firstResponse = false; + int numDocuments = 0; + boolean hasCompleted = false; + + @Override + public void onStart(StreamController controller) { + // No action needed in onStart + } + + @Override + public void onResponse(ExecutePipelineResponse response) { + if (!firstResponse) { + firstResponse = true; + currentSpan.addEvent( + TelemetryConstants.METHOD_NAME_EXECUTE_PIPELINE + ": First Response"); + metricsContext.recordLatency(MetricType.FIRST_RESPONSE_LATENCY); + } + + if (response.hasExplainStats()) { + resultObserver.setExplainStats( + new ExplainStats(response.getExplainStats().getData())); + } + + if (response.hasExecutionTime()) { + executionTime = Timestamp.fromProto(response.getExecutionTime()); + } + + if (response.getResultsCount() > 0) { + numDocuments += response.getResultsCount(); + if (numDocuments % NUM_RESPONSES_PER_TRACE_EVENT == 0) { + currentSpan.addEvent( + TelemetryConstants.METHOD_NAME_EXECUTE_PIPELINE + + ": Received " + + numDocuments + + " results"); + } + + for (Document doc : response.getResultsList()) { + resultObserver.onNext(PipelineResult.fromDocument(rpcContext, executionTime, doc)); + } + } + } + + @Override + public void onError(Throwable throwable) { + currentSpan.addEvent( + TelemetryConstants.METHOD_NAME_EXECUTE_PIPELINE + ": Error", + ImmutableMap.of("error.message", throwable.toString())); + metricsContext.recordLatency(MetricType.END_TO_END_LATENCY, throwable); + resultObserver.onError(throwable); + } + + @Override + public void onComplete() { + if (hasCompleted) { + return; + } + hasCompleted = true; + + metricsContext.recordLatency(MetricType.END_TO_END_LATENCY); + + currentSpan.addEvent( + TelemetryConstants.METHOD_NAME_EXECUTE_PIPELINE + ": Completed", + ImmutableMap.of(ATTRIBUTE_KEY_DOC_COUNT, numDocuments)); + resultObserver.onCompleted(executionTime); + } + }; + + logger.log(Level.FINEST, "Sending pipeline request: " + request.getStructuredPipeline()); + + rpcContext.streamRequest(request, observer, rpcContext.getClient().executePipelineCallable()); + } + + private interface ResultObserver extends ApiStreamObserver { + void onCompleted(Timestamp executionTime); + + void setExplainStats(ExplainStats explainStats); + + void setExecutionTime(Timestamp executionTime); + } + + @InternalExtensionOnly + abstract static class PipelineResultObserver implements ResultObserver { + private Timestamp executionTime; + private ExplainStats explainStats; + + @Override + public void onCompleted(Timestamp executionTime) { + this.executionTime = executionTime; + this.onCompleted(); + } + + public Timestamp getExecutionTime() { + return executionTime; + } + + public ExplainStats getExplainStats() { + return explainStats; + } + + @Override + public void setExplainStats(ExplainStats explainStats) { + this.explainStats = explainStats; + } + + @Override + public void setExecutionTime(Timestamp executionTime) { + this.executionTime = executionTime; + } + } +} diff --git a/google-cloud-firestore/src/main/java/com/google/cloud/firestore/PipelineResult.java b/google-cloud-firestore/src/main/java/com/google/cloud/firestore/PipelineResult.java new file mode 100644 index 000000000..166dd4a0f --- /dev/null +++ b/google-cloud-firestore/src/main/java/com/google/cloud/firestore/PipelineResult.java @@ -0,0 +1,456 @@ +/* + * 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 com.google.api.core.BetaApi; +import com.google.api.core.InternalExtensionOnly; +import com.google.cloud.Timestamp; +import com.google.cloud.firestore.encoding.CustomClassMapper; +import com.google.common.base.Preconditions; +import com.google.firestore.v1.Document; +import com.google.firestore.v1.Value; +import com.google.firestore.v1.Write; +import java.util.Date; +import java.util.HashMap; +import java.util.Iterator; +import java.util.Map; +import java.util.Objects; +import javax.annotation.Nonnull; +import javax.annotation.Nullable; + +/** + * A PipelineResult contains data read from a Firestore Pipeline. The data can be extracted with the + * {@link #getData()} or {@link #get(String)} methods. + * + *

If the PipelineResult represents a non-document result, getReference() will return a null + * value. + * + *

Subclassing Note: Firestore classes are not meant to be subclassed except for use in + * test mocks. Subclassing is not supported in production code and new SDK releases may break code + * that does so. + */ +@InternalExtensionOnly +@BetaApi +public final class PipelineResult { + + private final FirestoreRpcContext rpcContext; + @Nullable private final DocumentReference docRef; + @Nullable private final Map fields; + @Nonnull private final Timestamp executionTime; + @Nullable private final Timestamp updateTime; + @Nullable private final Timestamp createTime; + + PipelineResult( + FirestoreRpcContext rpcContext, + @Nullable DocumentReference docRef, + @Nullable Map fields, + @Nonnull Timestamp executionTime, + @Nullable Timestamp updateTime, + @Nullable Timestamp createTime) { // Elevated access level for mocking. + this.rpcContext = rpcContext; + this.docRef = docRef; + this.fields = fields; + this.executionTime = executionTime; + if (updateTime != null && updateTime.equals(Timestamp.ofTimeMicroseconds(0))) { + updateTime = null; + } + this.updateTime = updateTime; + + if (createTime != null && createTime.equals(Timestamp.ofTimeMicroseconds(0))) { + createTime = null; + } + this.createTime = createTime; + } + + /** + * Returns the ID of the document represented by this result. Returns null if this result is not + * corresponding to a Firestore document. + */ + @Nullable + @BetaApi + public String getId() { + return docRef.getId(); + } + + static PipelineResult fromDocument( + FirestoreRpcContext rpcContext, Timestamp executionTime, Document document) { + return new PipelineResult( + rpcContext, + document.getName().isEmpty() + ? null + : new DocumentReference(rpcContext, ResourcePath.create(document.getName())), + document.getFieldsMap(), + executionTime, + Timestamp.fromProto(document.getUpdateTime()), + Timestamp.fromProto(document.getCreateTime())); + } + + /** Returns the time at which the pipeline producing this result is executed. */ + @Nullable + @BetaApi + public Timestamp getExecutionTime() { + return executionTime; + } + + /** + * Returns the time at which this document was last updated. Returns null if this result is not + * corresponding to a Firestore document. + */ + @Nullable + @BetaApi + public Timestamp getUpdateTime() { + return updateTime; + } + + /** + * Returns the time at which this document was created. Returns null if this result is not + * corresponding to a Firestore document. + */ + @Nullable + @BetaApi + public Timestamp getCreateTime() { + return createTime; + } + + /** + * Returns whether or not the field exists in the document. Returns false if the document does not + * exist. + * + * @return whether the document existed in this snapshot. + */ + @BetaApi + public boolean exists() { + return fields != null; + } + + /** + * Returns the fields of the result as a Map or null if the result doesn't exist. Field values + * will be converted to their native Java representation. + * + * @return The fields of the document as a Map or null if the result doesn't exist. + */ + @Nonnull + @BetaApi + public Map getData() { + if (fields == null) { + return null; + } + + Map decodedFields = new HashMap<>(); + for (Map.Entry entry : fields.entrySet()) { + Object decodedValue = UserDataConverter.decodeValue(rpcContext, entry.getValue()); + decodedFields.put(entry.getKey(), decodedValue); + } + return decodedFields; + } + + /** + * Returns the contents of the document converted to a POJO or null if the result doesn't exist. + * + * @param valueType The Java class to create + * @return The contents of the document in an object of type T or null if the result doesn't + * exist. + */ + @Nullable + @BetaApi + T toObject(@Nonnull Class valueType) { + Map data = getData(); + return data == null ? null : CustomClassMapper.convertToCustomClass(data, valueType, docRef); + } + + /** + * Returns whether or not the field exists in the document. Returns false if the result does not + * exist. + * + * @param field the path to the field. + * @return true iff the field exists. + */ + @BetaApi + public boolean contains(@Nonnull String field) { + return contains(FieldPath.fromDotSeparatedString(field)); + } + + /** + * Returns whether or not the field exists in the document. Returns false if the result does not + * exist. + * + * @param fieldPath the path to the field. + * @return true iff the field exists. + */ + @BetaApi + public boolean contains(@Nonnull FieldPath fieldPath) { + return this.extractField(fieldPath) != null; + } + + /** + * Returns the value at the field or null if the field doesn't exist. + * + * @param field The path to the field. + * @return The value at the given field or null. + */ + @Nullable + @BetaApi + public Object get(@Nonnull String field) { + return get(FieldPath.fromDotSeparatedString(field)); + } + + /** + * Returns the value at the field, converted to a POJO, or null if the field or result doesn't + * exist. + * + * @param field The path to the field + * @param valueType The Java class to convert the field value to. + * @return The value at the given field or null. + */ + @Nullable + @BetaApi + public T get(@Nonnull String field, @Nonnull Class valueType) { + return get(FieldPath.fromDotSeparatedString(field), valueType); + } + + /** + * Returns the value at the field or null if the field doesn't exist. + * + * @param fieldPath The path to the field. + * @return The value at the given field or null. + */ + @Nullable + @BetaApi + public Object get(@Nonnull FieldPath fieldPath) { + Value value = extractField(fieldPath); + + if (value == null) { + return null; + } + + return UserDataConverter.decodeValue(rpcContext, value); + } + + /** + * Returns the value at the field, converted to a POJO, or null if the field or result doesn't + * exist. + * + * @param fieldPath The path to the field + * @param valueType The Java class to convert the field value to. + * @return The value at the given field or null. + */ + @Nullable + @BetaApi + public T get(@Nonnull FieldPath fieldPath, Class valueType) { + Object data = get(fieldPath); + return data == null ? null : CustomClassMapper.convertToCustomClass(data, valueType, docRef); + } + + /** Returns the Value Proto at 'fieldPath'. Returns null if the field was not found. */ + @Nullable + Value extractField(@Nonnull FieldPath fieldPath) { + Value value = null; + + if (fields != null) { + Iterator components = fieldPath.getSegments().iterator(); + value = fields.get(components.next()); + + while (value != null && components.hasNext()) { + if (value.getValueTypeCase() != Value.ValueTypeCase.MAP_VALUE) { + return null; + } + value = value.getMapValue().getFieldsOrDefault(components.next(), null); + } + } + + return value; + } + + /** + * Returns the value of the field as a boolean. + * + * @param field The path to the field. + * @throws RuntimeException if the value is not a Boolean. + * @return The value of the field. + */ + @Nullable + @BetaApi + public Boolean getBoolean(@Nonnull String field) { + return (Boolean) get(field); + } + + /** + * Returns the value of the field as a double. + * + * @param field The path to the field. + * @throws RuntimeException if the value is not a Number. + * @return The value of the field. + */ + @Nullable + @BetaApi + public Double getDouble(@Nonnull String field) { + Number number = (Number) get(field); + return number == null ? null : number.doubleValue(); + } + + /** + * Returns the value of the field as a String. + * + * @param field The path to the field. + * @throws RuntimeException if the value is not a String. + * @return The value of the field. + */ + @Nullable + @BetaApi + public String getString(@Nonnull String field) { + return (String) get(field); + } + + /** + * Returns the value of the field as a long. + * + * @param field The path to the field. + * @throws RuntimeException if the value is not a Number. + * @return The value of the field. + */ + @Nullable + @BetaApi + public Long getLong(@Nonnull String field) { + Number number = (Number) get(field); + return number == null ? null : number.longValue(); + } + + /** + * Returns the value of the field as a Date. + * + * @param field The path to the field. + * @throws RuntimeException if the value is not a Date. + * @return The value of the field. + */ + @Nullable + @BetaApi + public Date getDate(@Nonnull String field) { + Timestamp timestamp = getTimestamp(field); + return timestamp == null ? null : timestamp.toDate(); + } + + /** + * Returns the value of the field as a {@link Timestamp}. + * + * @param field The path to the field. + * @throws RuntimeException if the value is not a Date. + * @return The value of the field. + */ + @Nullable + @BetaApi + public Timestamp getTimestamp(@Nonnull String field) { + return (Timestamp) get(field); + } + + /** + * Returns the value of the field as a Blob. + * + * @param field The path to the field. + * @throws RuntimeException if the value is not a Blob. + * @return The value of the field. + */ + @Nullable + @BetaApi + public Blob getBlob(@Nonnull String field) { + return (Blob) get(field); + } + + /** + * Returns the value of the field as a GeoPoint. + * + * @param field The path to the field. + * @throws RuntimeException if the value is not a GeoPoint. + * @return The value of the field. + */ + @Nullable + @BetaApi + public GeoPoint getGeoPoint(@Nonnull String field) { + return (GeoPoint) get(field); + } + + /** + * Gets the reference to the document. + * + * @return The reference to the document. + */ + @BetaApi + public DocumentReference getReference() { + return docRef; + } + + /** Checks whether this DocumentSnapshot contains any fields. */ + boolean isEmpty() { + return fields == null || fields.isEmpty(); + } + + Map getProtoFields() { + return fields; + } + + Write.Builder toPb() { + Preconditions.checkState(exists(), "Can't call toDocument() on a document that doesn't exist"); + Write.Builder write = Write.newBuilder(); + Document.Builder document = write.getUpdateBuilder(); + document.setName(docRef.getName()); + document.putAllFields(fields); + return write; + } + + Document.Builder toDocumentPb() { + Preconditions.checkState(exists(), "Can't call toDocument() on a document that doesn't exist"); + Document.Builder document = Document.newBuilder(); + return document + .setName(docRef.getName()) + .putAllFields(fields) + .setCreateTime(createTime.toProto()) + .setUpdateTime(updateTime.toProto()); + } + + /** + * Returns true if the document's data and path in this DocumentSnapshot equals the provided + * snapshot. + * + * @param obj The object to compare against. + * @return Whether this DocumentSnapshot is equal to the provided object. + */ + @Override + @BetaApi + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (obj == null || !(obj instanceof PipelineResult)) { + return false; + } + PipelineResult that = (PipelineResult) obj; + return Objects.equals(rpcContext, that.rpcContext) + && Objects.equals(docRef, that.docRef) + && Objects.equals(fields, that.fields); + } + + @Override + public int hashCode() { + return Objects.hash(rpcContext, docRef, fields); + } + + @Override + public String toString() { + return String.format( + "%s{doc=%s, fields=%s, executionTime=%s, updateTime=%s, createTime=%s}", + getClass().getSimpleName(), docRef, fields, executionTime, updateTime, createTime); + } +} diff --git a/google-cloud-firestore/src/main/java/com/google/cloud/firestore/PipelineSource.java b/google-cloud-firestore/src/main/java/com/google/cloud/firestore/PipelineSource.java new file mode 100644 index 000000000..1c4d51255 --- /dev/null +++ b/google-cloud-firestore/src/main/java/com/google/cloud/firestore/PipelineSource.java @@ -0,0 +1,166 @@ +/* + * Copyright 2024 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.api.core.BetaApi; +import com.google.api.core.InternalApi; +import com.google.cloud.firestore.pipeline.stages.Collection; +import com.google.cloud.firestore.pipeline.stages.CollectionGroup; +import com.google.cloud.firestore.pipeline.stages.CollectionGroupOptions; +import com.google.cloud.firestore.pipeline.stages.CollectionOptions; +import com.google.cloud.firestore.pipeline.stages.Database; +import com.google.cloud.firestore.pipeline.stages.Documents; +import com.google.common.base.Preconditions; +import javax.annotation.Nonnull; + +/** + * A factory for creating {@link Pipeline} instances, which provide a framework for building data + * transformation and query pipelines for Firestore. + * + *

Start by calling {@link Firestore#pipeline()} to obtain an instance of {@code PipelineSource}. + * From there, you can use the provided methods (like {@link #collection(String)}) to specify the + * data source for your pipeline. + * + *

This class is typically used to start building Firestore pipelines. It allows you to define + * the initial data source for a pipeline. + * + *

Example Usage: + * + *

{@code
+ * firestore.pipeline() // Get a PipelineSource instance
+ *   .collection("users") // Create a pipeline that operates on a collection
+ *   .select("name"); // Add stages to the pipeline
+ * }
+ */ +@BetaApi +public final class PipelineSource { + private final FirestoreRpcContext rpcContext; + + @InternalApi + PipelineSource(FirestoreRpcContext rpcContext) { + this.rpcContext = rpcContext; + } + + /** + * Creates a new {@link Pipeline} that operates on the specified Firestore collection. + * + * @param path The path to the Firestore collection (e.g., "users"). + * @return A new {@code Pipeline} instance targeting the specified collection. + */ + @Nonnull + @BetaApi + public Pipeline collection(@Nonnull String path) { + return collection(path, new CollectionOptions()); + } + + @Nonnull + @BetaApi + public Pipeline collection(@Nonnull String path, CollectionOptions options) { + return new Pipeline(this.rpcContext, new Collection(path, options)); + } + + @Nonnull + @BetaApi + public Pipeline collection(@Nonnull CollectionReference ref) { + if (!this.rpcContext.getFirestore().equals(ref.getFirestore())) { + throw new IllegalArgumentException( + "Invalid CollectionReference. The Firestore instance of the CollectionReference must" + + " match the Firestore instance of the PipelineSource."); + } + + return collection(ref.getPath(), new CollectionOptions()); + } + + /** + * Creates a new {@link Pipeline} that operates on all documents in a collection group. + * + *

A collection group consists of all collections with the same ID. For example, if you have + * collections named "users" under different documents, you can query them together using a + * collection group query. + * + * @param collectionId The ID of the collection group. + * @return A new {@code Pipeline} instance targeting the specified collection group. + */ + @Nonnull + @BetaApi + public Pipeline collectionGroup(@Nonnull String collectionId) { + return collectionGroup(collectionId, new CollectionGroupOptions()); + } + + @Nonnull + @BetaApi + public Pipeline collectionGroup(@Nonnull String collectionId, CollectionGroupOptions options) { + Preconditions.checkArgument( + !collectionId.contains("/"), + "Invalid collectionId '%s'. Collection IDs must not contain '/'.", + collectionId); + return new Pipeline(this.rpcContext, new CollectionGroup(collectionId, options)); + } + + /** + * Creates a new {@link Pipeline} that operates on all documents in the Firestore database. + * + *

Use this method with caution as it can lead to very large result sets. It is usually only + * useful at development stage. + * + * @return A new {@code Pipeline} instance targeting all documents in the database. + */ + @Nonnull + @BetaApi + public Pipeline database() { + return new Pipeline(this.rpcContext, new Database()); + } + + /** + * Creates a new {@link Pipeline} that operates on a specific set of Firestore documents. + * + * @param docs The {@link DocumentReference} instances representing the documents to include in + * the pipeline. + * @return A new {@code Pipeline} instance targeting the specified documents. + */ + @Nonnull + @BetaApi + public Pipeline documents(DocumentReference... docs) { + return new Pipeline(this.rpcContext, Documents.of(docs)); + } + + /** + * Creates a new {@link Pipeline} from the given {@link Query}. Under the hood, this will + * translate the query semantics (order by document ID, etc.) to an equivalent pipeline. + * + * @param query The {@link Query} to translate into the resulting pipeline. + * @return A new {@code Pipeline} that is equivalent to the given query. + */ + @Nonnull + @BetaApi + public Pipeline createFrom(Query query) { + return query.pipeline(); + } + + /** + * Creates a new {@link Pipeline} from the given {@link AggregateQuery}. Under the hood, this will + * translate the query semantics (order by document ID, etc.) to an equivalent pipeline. + * + * @param query The {@link AggregateQuery} to translate into the resulting pipeline. + * @return A new {@code Pipeline} that is equivalent to the given query. + */ + @Nonnull + @BetaApi + public Pipeline createFrom(AggregateQuery query) { + return query.pipeline(); + } +} diff --git a/google-cloud-firestore/src/main/java/com/google/cloud/firestore/PipelineUtils.java b/google-cloud-firestore/src/main/java/com/google/cloud/firestore/PipelineUtils.java new file mode 100644 index 000000000..f056ba6f0 --- /dev/null +++ b/google-cloud-firestore/src/main/java/com/google/cloud/firestore/PipelineUtils.java @@ -0,0 +1,248 @@ +/* + * Copyright 2024 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.cloud.firestore.pipeline.expressions.AggregateFunction.countAll; +import static com.google.cloud.firestore.pipeline.expressions.Expression.and; +import static com.google.cloud.firestore.pipeline.expressions.Expression.arrayContainsAny; +import static com.google.cloud.firestore.pipeline.expressions.Expression.field; +import static com.google.cloud.firestore.pipeline.expressions.Expression.not; +import static com.google.cloud.firestore.pipeline.expressions.Expression.nullValue; +import static com.google.cloud.firestore.pipeline.expressions.Expression.or; +import static com.google.cloud.firestore.pipeline.expressions.FunctionUtils.aggregateFunctionToValue; +import static com.google.cloud.firestore.pipeline.expressions.FunctionUtils.exprToValue; + +import com.google.api.core.InternalApi; +import com.google.cloud.firestore.Query.ComparisonFilterInternal; +import com.google.cloud.firestore.Query.CompositeFilterInternal; +import com.google.cloud.firestore.Query.FilterInternal; +import com.google.cloud.firestore.Query.LimitType; +import com.google.cloud.firestore.Query.UnaryFilterInternal; +import com.google.cloud.firestore.pipeline.expressions.AggregateFunction; +import com.google.cloud.firestore.pipeline.expressions.AliasedAggregate; +import com.google.cloud.firestore.pipeline.expressions.AliasedExpression; +import com.google.cloud.firestore.pipeline.expressions.BooleanExpression; +import com.google.cloud.firestore.pipeline.expressions.Expression; +import com.google.cloud.firestore.pipeline.expressions.Field; +import com.google.cloud.firestore.pipeline.expressions.Selectable; +import com.google.common.collect.Lists; +import com.google.firestore.v1.Cursor; +import com.google.firestore.v1.MapValue; +import com.google.firestore.v1.Value; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +@InternalApi +public class PipelineUtils { + @InternalApi + public static Value encodeValue(Object value) { + return UserDataConverter.encodeValue(FieldPath.empty(), value, UserDataConverter.ARGUMENT); + } + + @InternalApi + public static Value encodeValue(Expression value) { + return exprToValue(value); + } + + @InternalApi + public static Value encodeValue(AggregateFunction value) { + return aggregateFunctionToValue(value); + } + + @InternalApi + public static Value encodeValue(String value) { + return Value.newBuilder().setStringValue(value).build(); + } + + @InternalApi + public static Value encodeValue(boolean value) { + return Value.newBuilder().setBooleanValue(value).build(); + } + + @InternalApi + public static Value encodeValue(long value) { + return Value.newBuilder().setIntegerValue(value).build(); + } + + @InternalApi + public static Value encodeValue(double value) { + return Value.newBuilder().setDoubleValue(value).build(); + } + + @InternalApi + public static Value encodeValue(Map options) { + return Value.newBuilder() + .setMapValue(MapValue.newBuilder().putAllFields(options).build()) + .build(); + } + + @InternalApi + static BooleanExpression toPipelineBooleanExpr(FilterInternal f) { + if (f instanceof ComparisonFilterInternal) { + ComparisonFilterInternal comparisonFilter = (ComparisonFilterInternal) f; + Field field = Field.ofServerPath(comparisonFilter.fieldReference.getFieldPath()); + Value value = comparisonFilter.value; + switch (comparisonFilter.operator) { + case LESS_THAN: + return and(field.exists(), field.lessThan(value)); + case LESS_THAN_OR_EQUAL: + return and(field.exists(), field.lessThanOrEqual(value)); + case GREATER_THAN: + return and(field.exists(), field.greaterThan(value)); + case GREATER_THAN_OR_EQUAL: + return and(field.exists(), field.greaterThanOrEqual(value)); + case EQUAL: + return and(field.exists(), field.equal(value)); + case NOT_EQUAL: + return and(field.exists(), field.notEqual(value)); + case ARRAY_CONTAINS: + return and(field.exists(), field.arrayContains(value)); + case IN: + List valuesList = value.getArrayValue().getValuesList(); + return and(field.exists(), Expression.equalAny(field, Lists.newArrayList(valuesList))); + case ARRAY_CONTAINS_ANY: + List valuesListAny = value.getArrayValue().getValuesList(); + return and(field.exists(), arrayContainsAny(field, Lists.newArrayList(valuesListAny))); + case NOT_IN: + List notInValues = value.getArrayValue().getValuesList(); + return and( + field.exists(), not(Expression.equalAny(field, Lists.newArrayList(notInValues)))); + default: + // Handle OPERATOR_UNSPECIFIED and UNRECOGNIZED cases as needed + throw new IllegalArgumentException("Unsupported operator: " + comparisonFilter.operator); + } + } else if (f instanceof CompositeFilterInternal) { + CompositeFilterInternal compositeFilter = (CompositeFilterInternal) f; + switch (compositeFilter.getOperator()) { + case AND: + List conditions = + compositeFilter.getFilters().stream() + .map(PipelineUtils::toPipelineBooleanExpr) + .collect(Collectors.toList()); + return and( + conditions.get(0), + conditions.subList(1, conditions.size()).toArray(new BooleanExpression[0])); + case OR: + List orConditions = + compositeFilter.getFilters().stream() + .map(PipelineUtils::toPipelineBooleanExpr) + .collect(Collectors.toList()); + return or( + orConditions.get(0), + orConditions.subList(1, orConditions.size()).toArray(new BooleanExpression[0])); + default: + // Handle OPERATOR_UNSPECIFIED and UNRECOGNIZED cases as needed + throw new IllegalArgumentException( + "Unsupported operator: " + compositeFilter.getOperator()); + } + } else if (f instanceof UnaryFilterInternal) { + UnaryFilterInternal unaryFilter = (UnaryFilterInternal) f; + Field field = Field.ofServerPath(unaryFilter.fieldReference.getFieldPath()); + switch (unaryFilter.getOperator()) { + case IS_NAN: + return and(field.exists(), field.equal(Double.NaN)); + case IS_NULL: + return and(field.exists(), field.equal(nullValue())); + case IS_NOT_NAN: + return and(field.exists(), field.notEqual(Double.NaN)); + case IS_NOT_NULL: + return and(field.exists(), field.notEqual(nullValue())); + default: + // Handle OPERATOR_UNSPECIFIED and UNRECOGNIZED cases as needed + throw new IllegalArgumentException("Unsupported operator: " + unaryFilter.getOperator()); + } + } else { + // Handle other FilterInternal types as needed + throw new IllegalArgumentException("Unsupported filter type: " + f.getClass().getName()); + } + } + + @InternalApi + static Pipeline toPaginatedPipeline( + Pipeline pipeline, + Cursor start, + Cursor end, + Integer limit, + LimitType limitType, + Integer offset) { + throw new UnsupportedOperationException( + "Converting to pagination pipeline is not support yet."); + } + + @InternalApi + static AliasedAggregate toPipelineAggregatorTarget(AggregateField f) { + String operator = f.getOperator(); + String fieldPath = f.getFieldPath(); + + switch (operator) { + case "sum": + return Field.ofServerPath(fieldPath).sum().as(f.getAlias()); + + case "count": + return countAll().as(f.getAlias()); + case "average": + return Field.ofServerPath(fieldPath).average().as(f.getAlias()); + default: + // Handle the 'else' case appropriately in your Java code + throw new IllegalArgumentException("Unsupported operator: " + operator); + } + } + + @InternalApi + static BooleanExpression toPipelineExistsExpr(AggregateField f) { + String fieldPath = f.getFieldPath(); + + if (fieldPath.isEmpty()) { + return null; + } + return Field.ofServerPath(fieldPath).exists(); + } + + @InternalApi + public static Map selectablesToMap(Selectable... selectables) { + Map projMap = new HashMap<>(); + for (Selectable proj : selectables) { + if (proj instanceof Field) { + Field fieldProj = (Field) proj; + if (projMap.containsKey(fieldProj.getPath().getEncodedPath())) { + throw new IllegalArgumentException( + "Duplicate alias or field name: " + fieldProj.getPath().getEncodedPath()); + } + projMap.put(fieldProj.getPath().getEncodedPath(), fieldProj); + } else if (proj instanceof AliasedExpression) { + AliasedExpression aliasedExpr = (AliasedExpression) proj; + if (projMap.containsKey(aliasedExpr.getAlias())) { + throw new IllegalArgumentException( + "Duplicate alias or field name: " + aliasedExpr.getAlias()); + } + projMap.put(aliasedExpr.getAlias(), aliasedExpr.getExpr()); + } + } + return projMap; + } + + @InternalApi + public static Map fieldNamesToMap(String... fields) { + Map projMap = new HashMap<>(); + for (String field : fields) { + projMap.put(field, field(field)); + } + return projMap; + } +} 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 2bf9e98f9..295a99d95 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 @@ -16,6 +16,10 @@ package com.google.cloud.firestore; +import static com.google.cloud.firestore.PipelineUtils.toPipelineBooleanExpr; +import static com.google.cloud.firestore.pipeline.expressions.Expression.and; +import static com.google.cloud.firestore.pipeline.expressions.Expression.or; +import static com.google.cloud.firestore.telemetry.TraceUtil.*; 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; @@ -35,6 +39,11 @@ import com.google.cloud.Timestamp; import com.google.cloud.firestore.Query.QueryOptions.Builder; import com.google.cloud.firestore.encoding.CustomClassMapper; +import com.google.cloud.firestore.pipeline.expressions.BooleanExpression; +import com.google.cloud.firestore.pipeline.expressions.Expression; +import com.google.cloud.firestore.pipeline.expressions.Field; +import com.google.cloud.firestore.pipeline.expressions.Ordering; +import com.google.cloud.firestore.pipeline.expressions.Selectable; import com.google.cloud.firestore.telemetry.MetricsUtil.MetricsContext; import com.google.cloud.firestore.telemetry.TelemetryConstants; import com.google.common.base.Preconditions; @@ -51,6 +60,7 @@ 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.StructuredQuery.UnaryFilter; import com.google.firestore.v1.Value; import com.google.protobuf.ByteString; import com.google.protobuf.Int32Value; @@ -161,6 +171,11 @@ public List getFilters() { return filters; } + @Nonnull + CompositeFilter.Operator getOperator() { + return this.operator; + } + @Nullable @Override public FieldReference getFirstInequalityField() { @@ -226,7 +241,7 @@ public List getFlattenedFilters() { } } - private static class UnaryFilterInternal extends FieldFilterInternal { + static class UnaryFilterInternal extends FieldFilterInternal { private final StructuredQuery.UnaryFilter.Operator operator; @@ -253,6 +268,11 @@ Filter toProto() { return result.build(); } + @Nonnull + UnaryFilter.Operator getOperator() { + return this.operator; + } + @Override public boolean equals(Object o) { if (this == o) { @@ -2003,6 +2023,144 @@ public VectorQuery findNearest( this, vectorField, queryVector, limit, distanceMeasure, vectorQueryOptions); } + Pipeline pipeline() { + // From + Pipeline ppl = + this.options.getAllDescendants() + ? new PipelineSource(this.rpcContext).collectionGroup(this.options.getCollectionId()) + : new PipelineSource(this.rpcContext) + .collection( + this.options.getParentPath().append(this.options.getCollectionId()).getPath()); + + // Filters + for (FilterInternal f : this.options.getFilters()) { + ppl = ppl.where(toPipelineBooleanExpr(f)); + } + + // Projections + if (this.options.getFieldProjections() != null + && !this.options.getFieldProjections().isEmpty()) { + ppl = + ppl.select( + this.options.getFieldProjections().stream() + .map(fieldReference -> Field.ofServerPath(fieldReference.getFieldPath())) + .toArray(Selectable[]::new)); + } + + // Orders + List normalizedOrderBy = createImplicitOrderBy(); + int size = normalizedOrderBy.size(); + List fields = new ArrayList<>(size); + List orderings = new ArrayList<>(size); + for (FieldOrder order : normalizedOrderBy) { + Field field = Field.ofServerPath(order.fieldReference.getFieldPath()); + fields.add(field); + if (order.direction == Direction.ASCENDING) { + orderings.add(field.ascending()); + } else { + orderings.add(field.descending()); + } + } + + if (fields.size() == 1) { + ppl = ppl.where(fields.get(0).exists()); + } else { + ppl = + ppl.where( + and( + fields.get(0).exists(), + fields.subList(1, fields.size()).stream() + .map((Field field) -> field.exists()) + .toArray(BooleanExpression[]::new))); + } + + // Cursors, Limit, Offset + if (this.options.getStartCursor() != null) { + ppl = ppl.where(whereConditionsFromCursor(options.getStartCursor(), orderings, true)); + } + + if (this.options.getEndCursor() != null) { + ppl = ppl.where(whereConditionsFromCursor(options.getEndCursor(), orderings, false)); + } + + if (options.getLimit() != null) { + // TODO: Handle situation where user enters limit larger than integer. + if (options.getLimitType() == LimitType.First) { + ppl = ppl.sort(orderings.toArray(new Ordering[0])); + ppl = ppl.limit(options.getLimit()); + } else { + if (options.getFieldOrders().isEmpty()) { + throw new IllegalStateException( + "limitToLast() queries require specifying at least one orderBy() clause"); + } + + List reversedOrderings = new ArrayList<>(); + for (Ordering ordering : orderings) { + reversedOrderings.add(reverseOrdering(ordering)); + } + ppl = ppl.sort(reversedOrderings.toArray(new Ordering[0])); + ppl = ppl.limit(options.getLimit()); + ppl = ppl.sort(orderings.toArray(new Ordering[0])); + } + } else { + ppl = ppl.sort(orderings.toArray(new Ordering[0])); + } + + return ppl; + } + + private static Ordering reverseOrdering(Ordering ordering) { + if (ordering.getDir() == Ordering.Direction.ASCENDING) { + return ordering.getExpr().descending(); + } else { + return ordering.getExpr().ascending(); + } + } + + private static BooleanExpression getCursorExclusiveCondition( + boolean isStart, Ordering ordering, Value value) { + if (isStart && ordering.getDir() == Ordering.Direction.ASCENDING + || !isStart && ordering.getDir() == Ordering.Direction.DESCENDING) { + return ordering.getExpr().greaterThan(value); + } else { + return ordering.getExpr().lessThan(value); + } + } + + private static BooleanExpression whereConditionsFromCursor( + Cursor bound, List orderings, boolean isStart) { + List boundPosition = bound.getValuesList(); + int size = boundPosition.size(); + if (size > orderings.size()) { + throw new IllegalArgumentException("Bound positions must not exceed order fields."); + } + + int last = size - 1; + BooleanExpression condition = + getCursorExclusiveCondition(isStart, orderings.get(last), boundPosition.get(last)); + if (isBoundInclusive(bound, isStart)) { + condition = + or(condition, Expression.equal(orderings.get(last).getExpr(), boundPosition.get(last))); + } + for (int i = size - 2; i >= 0; i--) { + final Ordering ordering = orderings.get(i); + final Value value = boundPosition.get(i); + condition = + or( + getCursorExclusiveCondition(isStart, ordering, value), + and(ordering.getExpr().equal(value), condition)); + } + return condition; + } + + private static boolean isBoundInclusive(Cursor bound, boolean isStart) { + if (isStart) { + return bound.getBefore(); + } else { + return !bound.getBefore(); + } + } + /** * Returns true if this Query is equal to the provided object. * diff --git a/google-cloud-firestore/src/main/java/com/google/cloud/firestore/ReadTimeTransaction.java b/google-cloud-firestore/src/main/java/com/google/cloud/firestore/ReadTimeTransaction.java index 8780e9c63..4c30caa4d 100644 --- a/google-cloud-firestore/src/main/java/com/google/cloud/firestore/ReadTimeTransaction.java +++ b/google-cloud-firestore/src/main/java/com/google/cloud/firestore/ReadTimeTransaction.java @@ -18,6 +18,7 @@ import com.google.api.core.ApiFuture; import com.google.api.core.ApiFutures; +import com.google.cloud.firestore.pipeline.stages.PipelineExecuteOptions; import com.google.cloud.firestore.telemetry.TelemetryConstants; import com.google.cloud.firestore.telemetry.TraceUtil; import com.google.common.base.Preconditions; @@ -129,6 +130,21 @@ public ApiFuture get(@Nonnull AggregateQuery query) { } } + @Nonnull + @Override + public ApiFuture execute(@Nonnull Pipeline pipeline) { + return execute(pipeline, new PipelineExecuteOptions()); + } + + @Nonnull + @Override + public ApiFuture execute( + @Nonnull Pipeline pipeline, @Nonnull PipelineExecuteOptions options) { + try (TraceUtil.Scope ignored = transactionTraceContext.makeCurrent()) { + return pipeline.execute(options, null, readTime); + } + } + @Nonnull @Override public Transaction create( diff --git a/google-cloud-firestore/src/main/java/com/google/cloud/firestore/ServerSideTransaction.java b/google-cloud-firestore/src/main/java/com/google/cloud/firestore/ServerSideTransaction.java index 40dd64ce8..1c89b2461 100644 --- a/google-cloud-firestore/src/main/java/com/google/cloud/firestore/ServerSideTransaction.java +++ b/google-cloud-firestore/src/main/java/com/google/cloud/firestore/ServerSideTransaction.java @@ -19,6 +19,7 @@ import com.google.api.core.ApiFuture; import com.google.api.core.ApiFutures; import com.google.cloud.firestore.TransactionOptions.TransactionOptionsType; +import com.google.cloud.firestore.pipeline.stages.PipelineExecuteOptions; import com.google.cloud.firestore.telemetry.TelemetryConstants; import com.google.cloud.firestore.telemetry.TraceUtil; import com.google.common.base.Preconditions; @@ -266,4 +267,19 @@ public ApiFuture get(@Nonnull AggregateQuery query) { return query.get(transactionId, null); } } + + @Nonnull + @Override + public ApiFuture execute(@Nonnull Pipeline pipeline) { + return execute(pipeline, new PipelineExecuteOptions()); + } + + @Nonnull + @Override + public ApiFuture execute( + @Nonnull Pipeline pipeline, @Nonnull PipelineExecuteOptions options) { + try (TraceUtil.Scope ignored = transactionTraceContext.makeCurrent()) { + return pipeline.execute(new PipelineExecuteOptions(), transactionId, null); + } + } } diff --git a/google-cloud-firestore/src/main/java/com/google/cloud/firestore/Transaction.java b/google-cloud-firestore/src/main/java/com/google/cloud/firestore/Transaction.java index ff1b7e2d6..7404c4e69 100644 --- a/google-cloud-firestore/src/main/java/com/google/cloud/firestore/Transaction.java +++ b/google-cloud-firestore/src/main/java/com/google/cloud/firestore/Transaction.java @@ -17,7 +17,9 @@ package com.google.cloud.firestore; import com.google.api.core.ApiFuture; +import com.google.api.core.BetaApi; import com.google.api.core.InternalExtensionOnly; +import com.google.cloud.firestore.pipeline.stages.PipelineExecuteOptions; import com.google.cloud.firestore.telemetry.MetricsUtil; import com.google.cloud.firestore.telemetry.TraceUtil; import com.google.cloud.firestore.telemetry.TraceUtil.Context; @@ -141,4 +143,19 @@ public abstract ApiFuture> getAll( */ @Nonnull public abstract ApiFuture get(@Nonnull AggregateQuery query); + + /** + * @return The result of the aggregation. + */ + @Nonnull + @BetaApi + public abstract ApiFuture execute(@Nonnull Pipeline pipeline); + + /** + * @return The result of the aggregation. + */ + @Nonnull + @BetaApi + public abstract ApiFuture execute( + @Nonnull Pipeline pipeline, @Nonnull PipelineExecuteOptions options); } diff --git a/google-cloud-firestore/src/main/java/com/google/cloud/firestore/UserDataConverter.java b/google-cloud-firestore/src/main/java/com/google/cloud/firestore/UserDataConverter.java index 45f2a6627..23673c348 100644 --- a/google-cloud-firestore/src/main/java/com/google/cloud/firestore/UserDataConverter.java +++ b/google-cloud-firestore/src/main/java/com/google/cloud/firestore/UserDataConverter.java @@ -16,7 +16,12 @@ package com.google.cloud.firestore; +import static com.google.cloud.firestore.pipeline.expressions.FunctionUtils.aggregateFunctionToValue; +import static com.google.cloud.firestore.pipeline.expressions.FunctionUtils.exprToValue; + import com.google.cloud.Timestamp; +import com.google.cloud.firestore.pipeline.expressions.AggregateFunction; +import com.google.cloud.firestore.pipeline.expressions.Expression; import com.google.common.base.Preconditions; import com.google.common.collect.Lists; import com.google.common.collect.Maps; @@ -38,6 +43,9 @@ /** Converts user input into the Firestore Value representation. */ class UserDataConverter { + + static final Value NULL_VALUE = Value.newBuilder().setNullValue(NullValue.NULL_VALUE).build(); + private static final Logger LOGGER = Logger.getLogger(UserDataConverter.class.getName()); /** Controls the behavior for field deletes. */ @@ -117,8 +125,9 @@ static Value encodeValue( + " as an argument at field '%s'.", path); return null; + } else if (sanitizedObject == null) { - return Value.newBuilder().setNullValue(NullValue.NULL_VALUE).build(); + return NULL_VALUE; } else if (sanitizedObject instanceof String) { return Value.newBuilder().setStringValue((String) sanitizedObject).build(); } else if (sanitizedObject instanceof Integer) { @@ -159,6 +168,10 @@ static Value encodeValue( } else if (sanitizedObject instanceof Blob) { Blob blob = (Blob) sanitizedObject; return Value.newBuilder().setBytesValue(blob.toByteString()).build(); + } else if (sanitizedObject instanceof Expression) { + return exprToValue((Expression) sanitizedObject); + } else if (sanitizedObject instanceof AggregateFunction) { + return aggregateFunctionToValue((AggregateFunction) sanitizedObject); } else if (sanitizedObject instanceof Value) { return (Value) sanitizedObject; } else if (sanitizedObject instanceof DocumentReference) { diff --git a/google-cloud-firestore/src/main/java/com/google/cloud/firestore/pipeline/expressions/AggregateFunction.java b/google-cloud-firestore/src/main/java/com/google/cloud/firestore/pipeline/expressions/AggregateFunction.java new file mode 100644 index 000000000..04e2c73f2 --- /dev/null +++ b/google-cloud-firestore/src/main/java/com/google/cloud/firestore/pipeline/expressions/AggregateFunction.java @@ -0,0 +1,129 @@ +/* + * Copyright 2025 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.pipeline.expressions; + +import com.google.api.core.BetaApi; +import com.google.common.collect.ImmutableList; +import com.google.firestore.v1.Value; +import java.util.stream.Collectors; + +@BetaApi +public class AggregateFunction { + private final String name; + private final ImmutableList params; + + private AggregateFunction(String name, Expression... params) { + this.name = name; + this.params = ImmutableList.copyOf(params); + } + + private AggregateFunction(String name, String fieldName) { + this(name, Expression.field(fieldName)); + } + + @BetaApi + public static AggregateFunction generic(String name, Expression... expr) { + return new AggregateFunction(name, expr); + } + + @BetaApi + public static AggregateFunction countAll() { + return new AggregateFunction("count"); + } + + @BetaApi + public static AggregateFunction count(String fieldName) { + return new AggregateFunction("count", fieldName); + } + + @BetaApi + public static AggregateFunction count(Expression expression) { + return new AggregateFunction("count", expression); + } + + @BetaApi + public static AggregateFunction countDistinct(String fieldName) { + return new AggregateFunction("count_distinct", fieldName); + } + + @BetaApi + public static AggregateFunction countDistinct(Expression expression) { + return new AggregateFunction("count_distinct", expression); + } + + @BetaApi + public static AggregateFunction countIf(BooleanExpression condition) { + return new AggregateFunction("count_if", condition); + } + + @BetaApi + public static AggregateFunction sum(String fieldName) { + return new AggregateFunction("sum", fieldName); + } + + @BetaApi + public static AggregateFunction sum(Expression expression) { + return new AggregateFunction("sum", expression); + } + + @BetaApi + public static AggregateFunction average(String fieldName) { + return new AggregateFunction("average", fieldName); + } + + @BetaApi + public static AggregateFunction average(Expression expression) { + return new AggregateFunction("average", expression); + } + + @BetaApi + public static AggregateFunction minimum(String fieldName) { + return new AggregateFunction("minimum", fieldName); + } + + @BetaApi + public static AggregateFunction minimum(Expression expression) { + return new AggregateFunction("minimum", expression); + } + + @BetaApi + public static AggregateFunction maximum(String fieldName) { + return new AggregateFunction("maximum", fieldName); + } + + @BetaApi + public static AggregateFunction maximum(Expression expression) { + return new AggregateFunction("maximum", expression); + } + + @BetaApi + public AliasedAggregate as(String alias) { + return new AliasedAggregate(alias, this); + } + + Value toProto() { + return Value.newBuilder() + .setFunctionValue( + com.google.firestore.v1.Function.newBuilder() + .setName(this.name) + .addAllArgs( + this.params.stream() + .map(FunctionUtils::exprToValue) + .collect(Collectors.toList()))) + .build(); + } +} diff --git a/google-cloud-firestore/src/main/java/com/google/cloud/firestore/pipeline/expressions/AliasedAggregate.java b/google-cloud-firestore/src/main/java/com/google/cloud/firestore/pipeline/expressions/AliasedAggregate.java new file mode 100644 index 000000000..573785972 --- /dev/null +++ b/google-cloud-firestore/src/main/java/com/google/cloud/firestore/pipeline/expressions/AliasedAggregate.java @@ -0,0 +1,38 @@ +/* + * Copyright 2025 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.pipeline.expressions; + +import com.google.api.core.BetaApi; + +@BetaApi +public class AliasedAggregate { + private final String alias; + private final AggregateFunction expr; + + AliasedAggregate(String alias, AggregateFunction expr) { + this.alias = alias; + this.expr = expr; + } + + public String getAlias() { + return alias; + } + + public AggregateFunction getExpr() { + return expr; + } +} diff --git a/google-cloud-firestore/src/main/java/com/google/cloud/firestore/pipeline/expressions/AliasedExpression.java b/google-cloud-firestore/src/main/java/com/google/cloud/firestore/pipeline/expressions/AliasedExpression.java new file mode 100644 index 000000000..f5ab70f21 --- /dev/null +++ b/google-cloud-firestore/src/main/java/com/google/cloud/firestore/pipeline/expressions/AliasedExpression.java @@ -0,0 +1,51 @@ +/* + * Copyright 2024 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.pipeline.expressions; + +import com.google.api.core.InternalApi; +import com.google.firestore.v1.Value; + +@InternalApi +public final class AliasedExpression implements Selectable { + + private final String alias; + private final Expression expr; + + @InternalApi + AliasedExpression(Expression expr, String alias) { + this.expr = expr; + this.alias = alias; + } + + @InternalApi + public String getAlias() { + return alias; + } + + @InternalApi + public Expression getExpr() { + return expr; + } + + public Selectable as(String alias) { + return new AliasedExpression(this.expr, alias); + } + + Value toProto() { + return expr.toProto(); + } +} diff --git a/google-cloud-firestore/src/main/java/com/google/cloud/firestore/pipeline/expressions/BooleanExpression.java b/google-cloud-firestore/src/main/java/com/google/cloud/firestore/pipeline/expressions/BooleanExpression.java new file mode 100644 index 000000000..1a674631d --- /dev/null +++ b/google-cloud-firestore/src/main/java/com/google/cloud/firestore/pipeline/expressions/BooleanExpression.java @@ -0,0 +1,93 @@ +/* + * Copyright 2025 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.pipeline.expressions; + +import com.google.api.core.BetaApi; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.Lists; + +@BetaApi +public class BooleanExpression extends FunctionExpression { + BooleanExpression(String name, Expression... params) { + super(name, Lists.newArrayList(params)); + } + + BooleanExpression(String name, ImmutableList params) { + super(name, params); + } + + /** + * Creates a conditional expression that evaluates to a {@code thenExpr} expression if this + * condition is true or an {@code elseExpr} expression if the condition is false. + * + * @param thenExpr The expression to evaluate if the condition is true. + * @param elseExpr The expression to evaluate if the condition is false. + * @return A new {@link Expression} representing the conditional operation. + */ + @BetaApi + public final Expression conditional(Expression thenExpr, Expression elseExpr) { + return conditional((BooleanExpression) this, thenExpr, elseExpr); + } + + /** + * Creates a conditional expression that evaluates to a {@code thenValue} if this condition is + * true or an {@code elseValue} if the condition is false. + * + * @param thenValue Value if the condition is true. + * @param elseValue Value if the condition is false. + * @return A new {@link Expression} representing the conditional operation. + */ + @BetaApi + public final Expression conditional(Object thenValue, Object elseValue) { + return conditional((BooleanExpression) this, thenValue, elseValue); + } + + /** + * Creates an expression that returns the {@code catchExpr} argument if there is an error, else + * return the result of this expression. + * + * @param catchExpr The catch expression that will be evaluated and returned if the this + * expression produces an error. + * @return A new {@link Expression} representing the ifError operation. + */ + @BetaApi + public final BooleanExpression ifError(BooleanExpression catchExpr) { + return ifError(this, catchExpr); + } + + /** + * Creates an expression that negates this boolean expression. + * + * @return A new {@link BooleanExpression} representing the not operation. + */ + @BetaApi + public final BooleanExpression not() { + return not(this); + } + + /** + * Creates a 'raw' boolean function expression. This is useful if the expression is available in + * the backend, but not yet in the current version of the SDK yet. + * + * @param name The name of the raw function. + * @param params The expressions to be passed as arguments to the function. + * @return A new [BooleanExpression] representing the raw function. + */ + public static BooleanExpression rawFunction(String name, Expression... params) { + return new BooleanExpression(name, params); + } +} diff --git a/google-cloud-firestore/src/main/java/com/google/cloud/firestore/pipeline/expressions/Constant.java b/google-cloud-firestore/src/main/java/com/google/cloud/firestore/pipeline/expressions/Constant.java new file mode 100644 index 000000000..94a5a73ad --- /dev/null +++ b/google-cloud-firestore/src/main/java/com/google/cloud/firestore/pipeline/expressions/Constant.java @@ -0,0 +1,39 @@ +/* + * Copyright 2024 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.pipeline.expressions; + +import static com.google.cloud.firestore.PipelineUtils.encodeValue; + +import com.google.api.core.BetaApi; +import com.google.firestore.v1.Value; + +@BetaApi +final class Constant extends Expression { + + static final Constant NULL = new Constant(null); + + private final Object value; + + Constant(Object value) { + this.value = value; + } + + @Override + Value toProto() { + return encodeValue(value); + } +} diff --git a/google-cloud-firestore/src/main/java/com/google/cloud/firestore/pipeline/expressions/Expression.java b/google-cloud-firestore/src/main/java/com/google/cloud/firestore/pipeline/expressions/Expression.java new file mode 100644 index 000000000..3142ab95b --- /dev/null +++ b/google-cloud-firestore/src/main/java/com/google/cloud/firestore/pipeline/expressions/Expression.java @@ -0,0 +1,4663 @@ +/* + * Copyright 2024 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.pipeline.expressions; + +import com.google.api.core.BetaApi; +import com.google.api.core.InternalApi; +import com.google.cloud.Timestamp; +import com.google.cloud.firestore.Blob; +import com.google.cloud.firestore.DocumentReference; +import com.google.cloud.firestore.FieldPath; +import com.google.cloud.firestore.FieldValue; +import com.google.cloud.firestore.GeoPoint; +import com.google.cloud.firestore.VectorValue; +import com.google.common.collect.ImmutableList; +import com.google.firestore.v1.Value; +import java.util.Arrays; +import java.util.Date; +import java.util.List; +import java.util.Map; + +/** + * Represents an expression that can be evaluated to a value within the execution of a {@link + * com.google.cloud.firestore.Pipeline}. + * + *

Expressions are the building blocks for creating complex queries and transformations in + * Firestore pipelines. They can represent: + * + *

    + *
  • **Field references:** Access values from document fields. + *
  • **Literals:** Represent constant values (strings, numbers, booleans). + *
  • **Function calls:** Apply functions to one or more expressions. + *
+ * + *

The `Expression` class provides a fluent API for building expressions. You can chain together + * method calls to create complex expressions. + */ +@BetaApi +public abstract class Expression { + + /** Constructor is package-private to prevent extension. */ + Expression() {} + + private static Expression toExprOrConstant(Object o) { + return o instanceof Expression ? (Expression) o : new Constant(o); + } + + private static ImmutableList toArrayOfExprOrConstant(Object... others) { + return Arrays.stream(others) + .map(Expression::toExprOrConstant) + .collect(ImmutableList.toImmutableList()); + } + + @InternalApi + abstract Value toProto(); + + // Constants + /** + * Create a constant for a {@link String} value. + * + * @param value The {@link String} value. + * @return A new {@link Expression} constant instance. + */ + @BetaApi + public static Expression constant(String value) { + return new Constant(value); + } + + /** + * Create a constant for a {@link Number} value. + * + * @param value The {@link Number} value. + * @return A new {@link Expression} constant instance. + */ + @BetaApi + public static Expression constant(Number value) { + return new Constant(value); + } + + /** + * Create a constant for a {@link Date} value. + * + * @param value The {@link Date} value. + * @return A new {@link Expression} constant instance. + */ + @BetaApi + public static Expression constant(Date value) { + return new Constant(value); + } + + /** + * Create a constant for a {@link Timestamp} value. + * + * @param value The {@link Timestamp} value. + * @return A new {@link Expression} constant instance. + */ + @BetaApi + public static Expression constant(Timestamp value) { + return new Constant(value); + } + + /** + * Create a constant for a {@link Boolean} value. + * + * @param value The {@link Boolean} value. + * @return A new {@link BooleanExpression} constant instance. + */ + @BetaApi + public static BooleanExpression constant(Boolean value) { + return equal(new Constant(value), true); + } + + /** + * Create a constant for a {@link GeoPoint} value. + * + * @param value The {@link GeoPoint} value. + * @return A new {@link Expression} constant instance. + */ + @BetaApi + public static Expression constant(GeoPoint value) { + return new Constant(value); + } + + /** + * Create a constant for a {@link Blob} value. + * + * @param value The {@link Blob} value. + * @return A new {@link Expression} constant instance. + */ + @BetaApi + public static Expression constant(Blob value) { + return new Constant(value); + } + + /** + * Create a constant for a {@link DocumentReference} value. + * + * @param value The {@link DocumentReference} value. + * @return A new {@link Expression} constant instance. + */ + @BetaApi + public static Expression constant(DocumentReference value) { + return new Constant(value); + } + + /** + * Create a constant for a bytes value. + * + * @param value The bytes value. + * @return A new {@link Expression} constant instance. + */ + @BetaApi + public static Expression constant(byte[] value) { + return new Constant(value); + } + + /** + * Create a constant for a {@link VectorValue} value. + * + * @param value The {@link VectorValue} value. + * @return A new {@link Expression} constant instance. + */ + @BetaApi + public static Expression constant(VectorValue value) { + return new Constant(value); + } + + /** + * Constant for a null value. + * + * @return An {@link Expression} constant instance. + */ + @BetaApi + public static Expression nullValue() { + return Constant.NULL; + } + + /** + * Create a vector constant for a {@code double[]} value. + * + * @param value The {@code double[]} value. + * @return An {@link Expression} constant instance. + */ + @BetaApi + public static Expression vector(double[] value) { + return new Constant(FieldValue.vector(value)); + } + + /** + * Create a vector constant for a {@link VectorValue} value. + * + * @param value The {@link VectorValue} value. + * @return An {@link Expression} constant instance. + */ + @BetaApi + public static Expression vector(VectorValue value) { + return new Constant(value); + } + + // Field Reference + /** + * Creates a {@link Field} instance representing the field at the given path. + * + *

The path can be a simple field name (e.g., "name") or a dot-separated path to a nested field + * (e.g., "address.city"). + * + * @param path The path to the field. + * @return A new {@link Field} instance representing the specified path. + */ + @BetaApi + public static Field field(String path) { + return Field.ofUserPath(path); + } + + /** + * Creates a {@link Field} instance representing the field at the given path. + * + *

The path can be a simple field name (e.g., "name") or a dot-separated path to a nested field + * (e.g., "address.city"). + * + * @param fieldPath The {@link FieldPath} to the field. + * @return A new {@link Field} instance representing the specified path. + */ + @BetaApi + public static Field field(FieldPath fieldPath) { + return Field.ofUserPath(fieldPath.toString()); + } + + /** + * Creates an expression that returns the current timestamp. + * + * @return A new {@link Expression} representing the current timestamp. + */ + @BetaApi + public static Expression currentTimestamp() { + return new FunctionExpression("current_timestamp", ImmutableList.of()); + } + + /** + * Creates an expression that returns a default value if an expression evaluates to an absent + * value. + * + * @param ifExpr The expression to check. + * @param elseExpr The default value. + * @return A new {@link Expression} representing the ifAbsent operation. + */ + @BetaApi + public static Expression ifAbsent(Expression ifExpr, Expression elseExpr) { + return new FunctionExpression("if_absent", ImmutableList.of(ifExpr, elseExpr)); + } + + /** + * Creates an expression that returns a default value if an expression evaluates to an absent + * value. + * + * @param ifExpr The expression to check. + * @param elseValue The default value. + * @return A new {@link Expression} representing the ifAbsent operation. + */ + @BetaApi + public static Expression ifAbsent(Expression ifExpr, Object elseValue) { + return ifAbsent(ifExpr, toExprOrConstant(elseValue)); + } + + /** + * Creates an expression that returns a default value if a field is absent. + * + * @param ifFieldName The field to check. + * @param elseExpr The default value. + * @return A new {@link Expression} representing the ifAbsent operation. + */ + @BetaApi + public static Expression ifAbsent(String ifFieldName, Expression elseExpr) { + return ifAbsent(field(ifFieldName), elseExpr); + } + + /** + * Creates an expression that returns a default value if a field is absent. + * + * @param ifFieldName The field to check. + * @param elseValue The default value. + * @return A new {@link Expression} representing the ifAbsent operation. + */ + @BetaApi + public static Expression ifAbsent(String ifFieldName, Object elseValue) { + return ifAbsent(field(ifFieldName), toExprOrConstant(elseValue)); + } + + /** + * Creates an expression that joins the elements of an array into a string. + * + * @param arrayExpression The expression representing the array. + * @param delimiter The delimiter to use. + * @return A new {@link Expression} representing the join operation. + */ + @BetaApi + public static Expression join(Expression arrayExpression, String delimiter) { + return new FunctionExpression("join", ImmutableList.of(arrayExpression, constant(delimiter))); + } + + /** + * Creates an expression that joins the elements of an array into a string. + * + * @param arrayExpression The expression representing the array. + * @param delimiterExpression The expression representing the delimiter. + * @return A new {@link Expression} representing the join operation. + */ + @BetaApi + public static Expression join(Expression arrayExpression, Expression delimiterExpression) { + return new FunctionExpression("join", ImmutableList.of(arrayExpression, delimiterExpression)); + } + + /** + * Creates an expression that joins the elements of an array into a string. + * + * @param arrayFieldName The field name of the array. + * @param delimiter The delimiter to use. + * @return A new {@link Expression} representing the join operation. + */ + @BetaApi + public static Expression join(String arrayFieldName, String delimiter) { + return join(field(arrayFieldName), constant(delimiter)); + } + + /** + * Creates an expression that joins the elements of an array into a string. + * + * @param arrayFieldName The field name of the array. + * @param delimiterExpression The expression representing the delimiter. + * @return A new {@link Expression} representing the join operation. + */ + @BetaApi + public static Expression join(String arrayFieldName, Expression delimiterExpression) { + return join(field(arrayFieldName), delimiterExpression); + } + + // Generic Function + /** + * Creates a generic function expression that is not yet implemented. + * + * @param name The name of the generic function. + * @param expr The expressions to be passed as arguments to the function. + * @return A new {@link Expression} representing the generic function. + */ + @BetaApi + public static Expression generic(String name, Expression... expr) { + return new FunctionExpression(name, ImmutableList.copyOf(expr)); + } + + // Logical Operators + /** + * Creates an expression that performs a logical 'AND' operation. + * + * @param condition The first {@link BooleanExpression}. + * @param conditions Additional {@link BooleanExpression}s. + * @return A new {@link BooleanExpression} representing the logical 'AND' operation. + */ + @BetaApi + public static BooleanExpression and( + BooleanExpression condition, BooleanExpression... conditions) { + ImmutableList.Builder builder = ImmutableList.builder(); + builder.add(condition); + builder.add(conditions); + return new BooleanExpression("and", builder.build()); + } + + /** + * Creates an expression that performs a logical 'OR' operation. + * + * @param condition The first {@link BooleanExpression}. + * @param conditions Additional {@link BooleanExpression}s. + * @return A new {@link BooleanExpression} representing the logical 'OR' operation. + */ + @BetaApi + public static BooleanExpression or(BooleanExpression condition, BooleanExpression... conditions) { + ImmutableList.Builder builder = ImmutableList.builder(); + builder.add(condition); + builder.add(conditions); + return new BooleanExpression("or", builder.build()); + } + + /** + * Creates an expression that performs a logical 'XOR' operation. + * + * @param condition The first {@link BooleanExpression}. + * @param conditions Additional {@link BooleanExpression}s. + * @return A new {@link BooleanExpression} representing the logical 'XOR' operation. + */ + @BetaApi + public static BooleanExpression xor( + BooleanExpression condition, BooleanExpression... conditions) { + ImmutableList.Builder builder = ImmutableList.builder(); + builder.add(condition); + builder.add(conditions); + return new BooleanExpression("xor", builder.build()); + } + + /** + * Creates an expression that negates a boolean expression. + * + * @param condition The boolean expression to negate. + * @return A new {@link BooleanExpression} representing the not operation. + */ + @BetaApi + public static BooleanExpression not(BooleanExpression condition) { + return new BooleanExpression("not", condition); + } + + // Arithmetic Operators + /** + * Creates an expression that adds numeric expressions. + * + * @param first Numeric expression to add. + * @param second Numeric expression to add. + * @return A new {@link Expression} representing the addition operation. + */ + @BetaApi + public static Expression add(Expression first, Expression second) { + return new FunctionExpression("add", ImmutableList.of(first, second)); + } + + /** + * Creates an expression that adds numeric expressions with a constant. + * + * @param first Numeric expression to add. + * @param second Constant to add. + * @return A new {@link Expression} representing the addition operation. + */ + @BetaApi + public static Expression add(Expression first, Number second) { + return add(first, constant(second)); + } + + /** + * Creates an expression that adds a numeric field with a numeric expression. + * + * @param fieldName Numeric field to add. + * @param second Numeric expression to add to field value. + * @return A new {@link Expression} representing the addition operation. + */ + @BetaApi + public static Expression add(String fieldName, Expression second) { + return add(field(fieldName), second); + } + + /** + * Creates an expression that adds a numeric field with constant. + * + * @param fieldName Numeric field to add. + * @param second Constant to add. + * @return A new {@link Expression} representing the addition operation. + */ + @BetaApi + public static Expression add(String fieldName, Number second) { + return add(field(fieldName), constant(second)); + } + + /** + * Creates an expression that subtracts two expressions. + * + * @param minuend Numeric expression to subtract from. + * @param subtrahend Numeric expression to subtract. + * @return A new {@link Expression} representing the subtract operation. + */ + @BetaApi + public static Expression subtract(Expression minuend, Expression subtrahend) { + return new FunctionExpression("subtract", ImmutableList.of(minuend, subtrahend)); + } + + /** + * Creates an expression that subtracts a constant value from a numeric expression. + * + * @param minuend Numeric expression to subtract from. + * @param subtrahend Constant to subtract. + * @return A new {@link Expression} representing the subtract operation. + */ + @BetaApi + public static Expression subtract(Expression minuend, Number subtrahend) { + return subtract(minuend, constant(subtrahend)); + } + + /** + * Creates an expression that subtracts a numeric expressions from numeric field. + * + * @param fieldName Numeric field to subtract from. + * @param subtrahend Numeric expression to subtract. + * @return A new {@link Expression} representing the subtract operation. + */ + @BetaApi + public static Expression subtract(String fieldName, Expression subtrahend) { + return subtract(field(fieldName), subtrahend); + } + + /** + * Creates an expression that subtracts a constant from numeric field. + * + * @param fieldName Numeric field to subtract from. + * @param subtrahend Constant to subtract. + * @return A new {@link Expression} representing the subtract operation. + */ + @BetaApi + public static Expression subtract(String fieldName, Number subtrahend) { + return subtract(field(fieldName), constant(subtrahend)); + } + + /** + * Creates an expression that multiplies numeric expressions. + * + * @param first Numeric expression to multiply. + * @param second Numeric expression to multiply. + * @return A new {@link Expression} representing the multiplication operation. + */ + @BetaApi + public static Expression multiply(Expression first, Expression second) { + return new FunctionExpression("multiply", ImmutableList.of(first, second)); + } + + /** + * Creates an expression that multiplies numeric expressions with a constant. + * + * @param first Numeric expression to multiply. + * @param second Constant to multiply. + * @return A new {@link Expression} representing the multiplication operation. + */ + @BetaApi + public static Expression multiply(Expression first, Number second) { + return multiply(first, constant(second)); + } + + /** + * Creates an expression that multiplies a numeric field with a numeric expression. + * + * @param fieldName Numeric field to multiply. + * @param second Numeric expression to multiply. + * @return A new {@link Expression} representing the multiplication operation. + */ + @BetaApi + public static Expression multiply(String fieldName, Expression second) { + return multiply(field(fieldName), second); + } + + /** + * Creates an expression that multiplies a numeric field with a constant. + * + * @param fieldName Numeric field to multiply. + * @param second Constant to multiply. + * @return A new {@link Expression} representing the multiplication operation. + */ + @BetaApi + public static Expression multiply(String fieldName, Number second) { + return multiply(field(fieldName), constant(second)); + } + + /** + * Creates an expression that divides two numeric expressions. + * + * @param dividend The numeric expression to be divided. + * @param divisor The numeric expression to divide by. + * @return A new {@link Expression} representing the division operation. + */ + @BetaApi + public static Expression divide(Expression dividend, Expression divisor) { + return new FunctionExpression("divide", ImmutableList.of(dividend, divisor)); + } + + /** + * Creates an expression that divides a numeric expression by a constant. + * + * @param dividend The numeric expression to be divided. + * @param divisor The constant to divide by. + * @return A new {@link Expression} representing the division operation. + */ + @BetaApi + public static Expression divide(Expression dividend, Number divisor) { + return divide(dividend, constant(divisor)); + } + + /** + * Creates an expression that divides numeric field by a numeric expression. + * + * @param fieldName The numeric field name to be divided. + * @param divisor The numeric expression to divide by. + * @return A new {@link Expression} representing the divide operation. + */ + @BetaApi + public static Expression divide(String fieldName, Expression divisor) { + return divide(field(fieldName), divisor); + } + + /** + * Creates an expression that divides a numeric field by a constant. + * + * @param fieldName The numeric field name to be divided. + * @param divisor The constant to divide by. + * @return A new {@link Expression} representing the divide operation. + */ + @BetaApi + public static Expression divide(String fieldName, Number divisor) { + return divide(field(fieldName), constant(divisor)); + } + + /** + * Creates an expression that calculates the modulo (remainder) of dividing two numeric + * expressions. + * + * @param dividend The numeric expression to be divided. + * @param divisor The numeric expression to divide by. + * @return A new {@link Expression} representing the modulo operation. + */ + @BetaApi + public static Expression mod(Expression dividend, Expression divisor) { + return new FunctionExpression("mod", ImmutableList.of(dividend, divisor)); + } + + /** + * Creates an expression that calculates the modulo (remainder) of dividing a numeric expression + * by a constant. + * + * @param dividend The numeric expression to be divided. + * @param divisor The constant to divide by. + * @return A new {@link Expression} representing the modulo operation. + */ + @BetaApi + public static Expression mod(Expression dividend, Number divisor) { + return mod(dividend, constant(divisor)); + } + + /** + * Creates an expression that calculates the modulo (remainder) of dividing a numeric field by a + * constant. + * + * @param fieldName The numeric field name to be divided. + * @param divisor The numeric expression to divide by. + * @return A new {@link Expression} representing the modulo operation. + */ + @BetaApi + public static Expression mod(String fieldName, Expression divisor) { + return mod(field(fieldName), divisor); + } + + /** + * Creates an expression that calculates the modulo (remainder) of dividing a numeric field by a + * constant. + * + * @param fieldName The numeric field name to be divided. + * @param divisor The constant to divide by. + * @return A new {@link Expression} representing the modulo operation. + */ + @BetaApi + public static Expression mod(String fieldName, Number divisor) { + return mod(field(fieldName), constant(divisor)); + } + + // Comparison Operators + /** + * Creates an expression that checks if two expressions are equal. + * + * @param left The first expression. + * @param right The second expression. + * @return A new {@link BooleanExpression} representing the equality comparison. + */ + @BetaApi + public static BooleanExpression equal(Expression left, Expression right) { + return new BooleanExpression("equal", left, right); + } + + /** + * Creates an expression that checks if an expression is equal to a constant value. + * + * @param left The expression. + * @param right The constant value. + * @return A new {@link BooleanExpression} representing the equality comparison. + */ + @BetaApi + public static BooleanExpression equal(Expression left, Object right) { + return new BooleanExpression("equal", left, toExprOrConstant(right)); + } + + /** + * Creates an expression that checks if a field is equal to an expression. + * + * @param fieldName The field name. + * @param right The expression. + * @return A new {@link BooleanExpression} representing the equality comparison. + */ + @BetaApi + public static BooleanExpression equal(String fieldName, Expression right) { + return equal(field(fieldName), right); + } + + /** + * Creates an expression that checks if a field is equal to a constant value. + * + * @param fieldName The field name. + * @param right The constant value. + * @return A new {@link BooleanExpression} representing the equality comparison. + */ + @BetaApi + public static BooleanExpression equal(String fieldName, Object right) { + return equal(field(fieldName), toExprOrConstant(right)); + } + + /** + * Creates an expression that checks if two expressions are not equal. + * + * @param left The first expression. + * @param right The second expression. + * @return A new {@link BooleanExpression} representing the inequality comparison. + */ + @BetaApi + public static BooleanExpression notEqual(Expression left, Expression right) { + return new BooleanExpression("not_equal", left, right); + } + + /** + * Creates an expression that checks if an expression is not equal to a constant value. + * + * @param left The expression. + * @param right The constant value. + * @return A new {@link BooleanExpression} representing the inequality comparison. + */ + @BetaApi + public static BooleanExpression notEqual(Expression left, Object right) { + return new BooleanExpression("not_equal", left, toExprOrConstant(right)); + } + + /** + * Creates an expression that checks if a field is not equal to an expression. + * + * @param fieldName The field name. + * @param right The expression. + * @return A new {@link BooleanExpression} representing the inequality comparison. + */ + @BetaApi + public static BooleanExpression notEqual(String fieldName, Expression right) { + return notEqual(field(fieldName), right); + } + + /** + * Creates an expression that checks if a field is not equal to a constant value. + * + * @param fieldName The field name. + * @param right The constant value. + * @return A new {@link BooleanExpression} representing the inequality comparison. + */ + @BetaApi + public static BooleanExpression notEqual(String fieldName, Object right) { + return notEqual(field(fieldName), toExprOrConstant(right)); + } + + /** + * Creates an expression that checks if the first expression is greater than the second + * expression. + * + * @param left The first expression. + * @param right The second expression. + * @return A new {@link BooleanExpression} representing the greater than comparison. + */ + @BetaApi + public static BooleanExpression greaterThan(Expression left, Expression right) { + return new BooleanExpression("greater_than", left, right); + } + + /** + * Creates an expression that checks if an expression is greater than a constant value. + * + * @param left The expression. + * @param right The constant value. + * @return A new {@link BooleanExpression} representing the greater than comparison. + */ + @BetaApi + public static BooleanExpression greaterThan(Expression left, Object right) { + return new BooleanExpression("greater_than", left, toExprOrConstant(right)); + } + + /** + * Creates an expression that checks if a field is greater than an expression. + * + * @param fieldName The field name. + * @param right The expression. + * @return A new {@link BooleanExpression} representing the greater than comparison. + */ + @BetaApi + public static BooleanExpression greaterThan(String fieldName, Expression right) { + return greaterThan(field(fieldName), right); + } + + /** + * Creates an expression that checks if a field is greater than a constant value. + * + * @param fieldName The field name. + * @param right The constant value. + * @return A new {@link BooleanExpression} representing the greater than comparison. + */ + @BetaApi + public static BooleanExpression greaterThan(String fieldName, Object right) { + return greaterThan(field(fieldName), toExprOrConstant(right)); + } + + /** + * Creates an expression that checks if the first expression is greater than or equal to the + * second expression. + * + * @param left The first expression. + * @param right The second expression. + * @return A new {@link BooleanExpression} representing the greater than or equal to comparison. + */ + @BetaApi + public static BooleanExpression greaterThanOrEqual(Expression left, Expression right) { + return new BooleanExpression("greater_than_or_equal", left, right); + } + + /** + * Creates an expression that checks if an expression is greater than or equal to a constant + * value. + * + * @param left The expression. + * @param right The constant value. + * @return A new {@link BooleanExpression} representing the greater than or equal to comparison. + */ + @BetaApi + public static BooleanExpression greaterThanOrEqual(Expression left, Object right) { + return new BooleanExpression("greater_than_or_equal", left, toExprOrConstant(right)); + } + + /** + * Creates an expression that checks if a field is greater than or equal to an expression. + * + * @param fieldName The field name. + * @param right The expression. + * @return A new {@link BooleanExpression} representing the greater than or equal to comparison. + */ + @BetaApi + public static BooleanExpression greaterThanOrEqual(String fieldName, Expression right) { + return greaterThanOrEqual(field(fieldName), right); + } + + /** + * Creates an expression that checks if a field is greater than or equal to a constant value. + * + * @param fieldName The field name. + * @param right The constant value. + * @return A new {@link BooleanExpression} representing the greater than or equal to comparison. + */ + @BetaApi + public static BooleanExpression greaterThanOrEqual(String fieldName, Object right) { + return greaterThanOrEqual(field(fieldName), toExprOrConstant(right)); + } + + /** + * Creates an expression that checks if the first expression is less than the second expression. + * + * @param left The first expression. + * @param right The second expression. + * @return A new {@link BooleanExpression} representing the less than comparison. + */ + @BetaApi + public static BooleanExpression lessThan(Expression left, Expression right) { + return new BooleanExpression("less_than", left, right); + } + + /** + * Creates an expression that checks if an expression is less than a constant value. + * + * @param left The expression. + * @param right The constant value. + * @return A new {@link BooleanExpression} representing the less than comparison. + */ + @BetaApi + public static BooleanExpression lessThan(Expression left, Object right) { + return new BooleanExpression("less_than", left, toExprOrConstant(right)); + } + + /** + * Creates an expression that checks if a field is less than an expression. + * + * @param fieldName The field name. + * @param right The expression. + * @return A new {@link BooleanExpression} representing the less than comparison. + */ + @BetaApi + public static BooleanExpression lessThan(String fieldName, Expression right) { + return lessThan(field(fieldName), right); + } + + /** + * Creates an expression that checks if a field is less than a constant value. + * + * @param fieldName The field name. + * @param right The constant value. + * @return A new {@link BooleanExpression} representing the less than comparison. + */ + @BetaApi + public static BooleanExpression lessThan(String fieldName, Object right) { + return lessThan(field(fieldName), toExprOrConstant(right)); + } + + /** + * Creates an expression that checks if the first expression is less than or equal to the second + * expression. + * + * @param left The first expression. + * @param right The second expression. + * @return A new {@link BooleanExpression} representing the less than or equal to comparison. + */ + @BetaApi + public static BooleanExpression lessThanOrEqual(Expression left, Expression right) { + return new BooleanExpression("less_than_or_equal", left, right); + } + + /** + * Creates an expression that checks if an expression is less than or equal to a constant value. + * + * @param left The expression. + * @param right The constant value. + * @return A new {@link BooleanExpression} representing the less than or equal to comparison. + */ + @BetaApi + public static BooleanExpression lessThanOrEqual(Expression left, Object right) { + return new BooleanExpression("less_than_or_equal", left, toExprOrConstant(right)); + } + + /** + * Creates an expression that checks if a field is less than or equal to an expression. + * + * @param fieldName The field name. + * @param right The expression. + * @return A new {@link BooleanExpression} representing the less than or equal to comparison. + */ + @BetaApi + public static BooleanExpression lessThanOrEqual(String fieldName, Expression right) { + return lessThanOrEqual(field(fieldName), right); + } + + /** + * Creates an expression that checks if a field is less than or equal to a constant value. + * + * @param fieldName The field name. + * @param right The constant value. + * @return A new {@link BooleanExpression} representing the less than or equal to comparison. + */ + @BetaApi + public static BooleanExpression lessThanOrEqual(String fieldName, Object right) { + return lessThanOrEqual(field(fieldName), toExprOrConstant(right)); + } + + /** + * Creates an expression that checks if an {@code expression}, when evaluated, is equal to any of + * the provided {@code values}. + * + * @param expression The expression whose results to compare. + * @param values The values to check against. + * @return A new {@link BooleanExpression} representing the 'IN' comparison. + */ + @BetaApi + public static BooleanExpression equalAny(Expression expression, List values) { + return new BooleanExpression( + "equal_any", + expression, + new FunctionExpression("array", toArrayOfExprOrConstant(values.toArray()))); + } + + /** + * Creates an expression that checks if an {@code expression}, when evaluated, is equal to any of + * the elements of {@code arrayExpression}. + * + * @param expression The expression whose results to compare. + * @param arrayExpression An expression that evaluates to an array, whose elements to check for + * equality to the input. + * @return A new {@link BooleanExpression} representing the 'IN' comparison. + */ + @BetaApi + public static BooleanExpression equalAny(Expression expression, Expression arrayExpression) { + return new BooleanExpression("equal_any", expression, arrayExpression); + } + + /** + * Creates an expression that checks if a field's value is equal to any of the provided {@code + * values}. + * + * @param fieldName The field to compare. + * @param values The values to check against. + * @return A new {@link BooleanExpression} representing the 'IN' comparison. + */ + @BetaApi + public static BooleanExpression equalAny(String fieldName, List values) { + return equalAny( + field(fieldName), + new FunctionExpression("array", toArrayOfExprOrConstant(values.toArray()))); + } + + /** + * Creates an expression that checks if a field's value is equal to any of the elements of {@code + * arrayExpression}. + * + * @param fieldName The field to compare. + * @param arrayExpression An expression that evaluates to an array, whose elements to check for + * equality to the input. + * @return A new {@link BooleanExpression} representing the 'IN' comparison. + */ + @BetaApi + public static BooleanExpression equalAny(String fieldName, Expression arrayExpression) { + return equalAny(field(fieldName), arrayExpression); + } + + /** + * Creates an expression that checks if an {@code expression}, when evaluated, is not equal to all + * the provided {@code values}. + * + * @param expression The expression whose results to compare. + * @param values The values to check against. + * @return A new {@link BooleanExpression} representing the 'NOT IN' comparison. + */ + @BetaApi + public static BooleanExpression notEqualAny(Expression expression, List values) { + return new BooleanExpression( + "not_equal_any", + expression, + new FunctionExpression("array", toArrayOfExprOrConstant(values.toArray()))); + } + + /** + * Creates an expression that checks if an {@code expression}, when evaluated, is not equal to all + * the elements of {@code arrayExpression}. + * + * @param expression The expression whose results to compare. + * @param arrayExpression An expression that evaluates to an array, whose elements to check for + * equality to the input. + * @return A new {@link BooleanExpression} representing the 'NOT IN' comparison. + */ + @BetaApi + public static BooleanExpression notEqualAny(Expression expression, Expression arrayExpression) { + return new BooleanExpression("not_equal_any", expression, arrayExpression); + } + + /** + * Creates an expression that checks if a field's value is not equal to all of the provided {@code + * values}. + * + * @param fieldName The field to compare. + * @param values The values to check against. + * @return A new {@link BooleanExpression} representing the 'NOT IN' comparison. + */ + @BetaApi + public static BooleanExpression notEqualAny(String fieldName, List values) { + return notEqualAny( + field(fieldName), + new FunctionExpression("array", toArrayOfExprOrConstant(values.toArray()))); + } + + /** + * Creates an expression that checks if a field's value is not equal to all of the elements of + * {@code arrayExpression}. + * + * @param fieldName The field to compare. + * @param arrayExpression An expression that evaluates to an array, whose elements to check for + * equality to the input. + * @return A new {@link BooleanExpression} representing the 'NOT IN' comparison. + */ + @BetaApi + public static BooleanExpression notEqualAny(String fieldName, Expression arrayExpression) { + return notEqualAny(field(fieldName), arrayExpression); + } + + // String Functions + /** + * Creates an expression that calculates the character length of a string expression in UTF8. + * + * @param string The expression representing the string. + * @return A new {@link Expression} representing the charLength operation. + */ + @BetaApi + public static Expression charLength(Expression string) { + return new FunctionExpression("char_length", ImmutableList.of(string)); + } + + /** + * Creates an expression that calculates the character length of a string field in UTF8. + * + * @param fieldName The name of the field containing the string. + * @return A new {@link Expression} representing the charLength operation. + */ + @BetaApi + public static Expression charLength(String fieldName) { + return charLength(field(fieldName)); + } + + /** + * Creates an expression that calculates the length of a string in UTF-8 bytes, or just the length + * of a Blob. + * + * @param string The expression representing the string. + * @return A new {@link Expression} representing the length of the string in bytes. + */ + @BetaApi + public static Expression byteLength(Expression string) { + return new FunctionExpression("byte_length", ImmutableList.of(string)); + } + + /** + * Creates an expression that calculates the length of a string represented by a field in UTF-8 + * bytes, or just the length of a Blob. + * + * @param fieldName The name of the field containing the string. + * @return A new {@link Expression} representing the length of the string in bytes. + */ + @BetaApi + public static Expression byteLength(String fieldName) { + return byteLength(field(fieldName)); + } + + /** + * Creates an expression that calculates the length of string, array, map, vector, or Blob. + * + * @param string The expression representing the value to calculate the length of. + * @return A new {@link Expression} representing the length of the value. + */ + @BetaApi + public static Expression length(Expression string) { + return new FunctionExpression("length", ImmutableList.of(string)); + } + + /** + * Creates an expression that calculates the length of string, array, map, vector, or Blob. + * + * @param fieldName The name of the field containing the value. + * @return A new {@link Expression} representing the length of the value. + */ + @BetaApi + public static Expression length(String fieldName) { + return byteLength(field(fieldName)); + } + + /** + * Creates an expression that performs a case-sensitive wildcard string comparison. + * + * @param string The expression representing the string to perform the comparison on. + * @param pattern The pattern to search for. You can use "%" as a wildcard character. + * @return A new {@link BooleanExpression} representing the like operation. + */ + @BetaApi + public static BooleanExpression like(Expression string, Expression pattern) { + return new BooleanExpression("like", string, pattern); + } + + /** + * Creates an expression that performs a case-sensitive wildcard string comparison. + * + * @param string The expression representing the string to perform the comparison on. + * @param pattern The pattern to search for. You can use "%" as a wildcard character. + * @return A new {@link BooleanExpression} representing the like operation. + */ + @BetaApi + public static BooleanExpression like(Expression string, String pattern) { + return like(string, constant(pattern)); + } + + /** + * Creates an expression that performs a case-sensitive wildcard string comparison against a + * field. + * + * @param fieldName The name of the field containing the string. + * @param pattern The pattern to search for. You can use "%" as a wildcard character. + * @return A new {@link BooleanExpression} representing the like comparison. + */ + @BetaApi + public static BooleanExpression like(String fieldName, Expression pattern) { + return like(field(fieldName), pattern); + } + + /** + * Creates an expression that performs a case-sensitive wildcard string comparison against a + * field. + * + * @param fieldName The name of the field containing the string. + * @param pattern The pattern to search for. You can use "%" as a wildcard character. + * @return A new {@link BooleanExpression} representing the like comparison. + */ + @BetaApi + public static BooleanExpression like(String fieldName, String pattern) { + return like(field(fieldName), constant(pattern)); + } + + /** + * Creates an expression that checks if a string expression contains a specified regular + * expression as a substring. + * + * @param string The expression representing the string to perform the comparison on. + * @param pattern The regular expression to use for the search. + * @return A new {@link BooleanExpression} representing the contains regular expression + * comparison. + */ + @BetaApi + public static BooleanExpression regexContains(Expression string, Expression pattern) { + return new BooleanExpression("regex_contains", string, pattern); + } + + /** + * Creates an expression that checks if a string expression contains a specified regular + * expression as a substring. + * + * @param string The expression representing the string to perform the comparison on. + * @param pattern The regular expression to use for the search. + * @return A new {@link BooleanExpression} representing the contains regular expression + * comparison. + */ + @BetaApi + public static BooleanExpression regexContains(Expression string, String pattern) { + return regexContains(string, constant(pattern)); + } + + /** + * Creates an expression that checks if a string field contains a specified regular expression as + * a substring. + * + * @param fieldName The name of the field containing the string. + * @param pattern The regular expression to use for the search. + * @return A new {@link BooleanExpression} representing the contains regular expression + * comparison. + */ + @BetaApi + public static BooleanExpression regexContains(String fieldName, Expression pattern) { + return regexContains(field(fieldName), pattern); + } + + /** + * Creates an expression that checks if a string field contains a specified regular expression as + * a substring. + * + * @param fieldName The name of the field containing the string. + * @param pattern The regular expression to use for the search. + * @return A new {@link BooleanExpression} representing the contains regular expression + * comparison. + */ + @BetaApi + public static BooleanExpression regexContains(String fieldName, String pattern) { + return regexContains(field(fieldName), constant(pattern)); + } + + /** + * Creates an expression that checks if a string field matches a specified regular expression. + * + * @param string The expression representing the string to match against. + * @param pattern The regular expression to use for the match. + * @return A new {@link BooleanExpression} representing the regular expression match comparison. + */ + @BetaApi + public static BooleanExpression regexMatch(Expression string, Expression pattern) { + return new BooleanExpression("regex_match", string, pattern); + } + + /** + * Creates an expression that checks if a string field matches a specified regular expression. + * + * @param string The expression representing the string to match against. + * @param pattern The regular expression to use for the match. + * @return A new {@link BooleanExpression} representing the regular expression match comparison. + */ + @BetaApi + public static BooleanExpression regexMatch(Expression string, String pattern) { + return regexMatch(string, constant(pattern)); + } + + /** + * Creates an expression that checks if a string field matches a specified regular expression. + * + * @param fieldName The name of the field containing the string. + * @param pattern The regular expression to use for the match. + * @return A new {@link BooleanExpression} representing the regular expression match comparison. + */ + @BetaApi + public static BooleanExpression regexMatch(String fieldName, Expression pattern) { + return regexMatch(field(fieldName), pattern); + } + + /** + * Creates an expression that checks if a string field matches a specified regular expression. + * + * @param fieldName The name of the field containing the string. + * @param pattern The regular expression to use for the match. + * @return A new {@link BooleanExpression} representing the regular expression match comparison. + */ + @BetaApi + public static BooleanExpression regexMatch(String fieldName, String pattern) { + return regexMatch(field(fieldName), constant(pattern)); + } + + /** + * Creates an expression that checks if a string expression contains a specified substring. + * + * @param string The expression representing the string to perform the comparison on. + * @param substring The expression representing the substring to search for. + * @return A new {@link BooleanExpression} representing the contains comparison. + */ + @BetaApi + public static BooleanExpression stringContains(Expression string, Expression substring) { + return new BooleanExpression("string_contains", string, substring); + } + + /** + * Creates an expression that checks if a string expression contains a specified substring. + * + * @param string The expression representing the string to perform the comparison on. + * @param substring The substring to search for. + * @return A new {@link BooleanExpression} representing the contains comparison. + */ + @BetaApi + public static BooleanExpression stringContains(Expression string, String substring) { + return stringContains(string, constant(substring)); + } + + /** + * Creates an expression that checks if a string field contains a specified substring. + * + * @param fieldName The name of the field to perform the comparison on. + * @param substring The expression representing the substring to search for. + * @return A new {@link BooleanExpression} representing the contains comparison. + */ + @BetaApi + public static BooleanExpression stringContains(String fieldName, Expression substring) { + return stringContains(field(fieldName), substring); + } + + /** + * Creates an expression that checks if a string field contains a specified substring. + * + * @param fieldName The name of the field to perform the comparison on. + * @param substring The substring to search for. + * @return A new {@link BooleanExpression} representing the contains comparison. + */ + @BetaApi + public static BooleanExpression stringContains(String fieldName, String substring) { + return stringContains(field(fieldName), constant(substring)); + } + + /** + * Creates an expression that checks if a string expression starts with a given {@code prefix}. + * + * @param string The expression to check. + * @param prefix The prefix string expression to check for. + * @return A new {@link BooleanExpression} representing the 'starts with' comparison. + */ + @BetaApi + public static BooleanExpression startsWith(Expression string, Expression prefix) { + return new BooleanExpression("starts_with", string, prefix); + } + + /** + * Creates an expression that checks if a string expression starts with a given {@code prefix}. + * + * @param string The expression to check. + * @param prefix The prefix string to check for. + * @return A new {@link BooleanExpression} representing the 'starts with' comparison. + */ + @BetaApi + public static BooleanExpression startsWith(Expression string, String prefix) { + return startsWith(string, constant(prefix)); + } + + /** + * Creates an expression that checks if a string expression starts with a given {@code prefix}. + * + * @param fieldName The name of field that contains a string to check. + * @param prefix The prefix string expression to check for. + * @return A new {@link BooleanExpression} representing the 'starts with' comparison. + */ + @BetaApi + public static BooleanExpression startsWith(String fieldName, Expression prefix) { + return startsWith(field(fieldName), prefix); + } + + /** + * Creates an expression that checks if a string expression starts with a given {@code prefix}. + * + * @param fieldName The name of field that contains a string to check. + * @param prefix The prefix string to check for. + * @return A new {@link BooleanExpression} representing the 'starts with' comparison. + */ + @BetaApi + public static BooleanExpression startsWith(String fieldName, String prefix) { + return startsWith(field(fieldName), constant(prefix)); + } + + /** + * Creates an expression that checks if a string expression ends with a given {@code suffix}. + * + * @param string The expression to check. + * @param suffix The suffix string expression to check for. + * @return A new {@link BooleanExpression} representing the 'ends with' comparison. + */ + @BetaApi + public static BooleanExpression endsWith(Expression string, Expression suffix) { + return new BooleanExpression("ends_with", string, suffix); + } + + /** + * Creates an expression that checks if a string expression ends with a given {@code suffix}. + * + * @param string The expression to check. + * @param suffix The suffix string to check for. + * @return A new {@link BooleanExpression} representing the 'ends with' comparison. + */ + @BetaApi + public static BooleanExpression endsWith(Expression string, String suffix) { + return endsWith(string, constant(suffix)); + } + + /** + * Creates an expression that checks if a string expression ends with a given {@code suffix}. + * + * @param fieldName The name of field that contains a string to check. + * @param suffix The suffix string expression to check for. + * @return A new {@link BooleanExpression} representing the 'ends with' comparison. + */ + @BetaApi + public static BooleanExpression endsWith(String fieldName, Expression suffix) { + return endsWith(field(fieldName), suffix); + } + + /** + * Creates an expression that checks if a string expression ends with a given {@code suffix}. + * + * @param fieldName The name of field that contains a string to check. + * @param suffix The suffix string to check for. + * @return A new {@link BooleanExpression} representing the 'ends with' comparison. + */ + @BetaApi + public static BooleanExpression endsWith(String fieldName, String suffix) { + return endsWith(field(fieldName), constant(suffix)); + } + + /** + * Creates an expression that returns a substring of the given string. + * + * @param string The expression representing the string to get a substring from. + * @param index The starting index of the substring. + * @param length The length of the substring. + * @return A new {@link Expression} representing the substring. + */ + @BetaApi + public static Expression substring(Expression string, Expression index, Expression length) { + return new FunctionExpression("substring", ImmutableList.of(string, index, length)); + } + + /** + * Creates an expression that returns a substring of the given string. + * + * @param fieldName The name of the field containing the string to get a substring from. + * @param index The starting index of the substring. + * @param length The length of the substring. + * @return A new {@link Expression} representing the substring. + */ + @BetaApi + public static Expression substring(String fieldName, int index, int length) { + return substring(field(fieldName), constant(index), constant(length)); + } + + /** + * Creates an expression that converts a string expression to lowercase. + * + * @param string The expression representing the string to convert to lowercase. + * @return A new {@link Expression} representing the lowercase string. + */ + @BetaApi + public static Expression toLower(Expression string) { + return new FunctionExpression("to_lower", ImmutableList.of(string)); + } + + /** + * Creates an expression that converts a string field to lowercase. + * + * @param fieldName The name of the field containing the string to convert to lowercase. + * @return A new {@link Expression} representing the lowercase string. + */ + @BetaApi + public static Expression toLower(String fieldName) { + return toLower(field(fieldName)); + } + + /** + * Creates an expression that converts a string expression to uppercase. + * + * @param string The expression representing the string to convert to uppercase. + * @return A new {@link Expression} representing the lowercase string. + */ + @BetaApi + public static Expression toUpper(Expression string) { + return new FunctionExpression("to_upper", ImmutableList.of(string)); + } + + /** + * Creates an expression that converts a string field to uppercase. + * + * @param fieldName The name of the field containing the string to convert to uppercase. + * @return A new {@link Expression} representing the lowercase string. + */ + @BetaApi + public static Expression toUpper(String fieldName) { + return toUpper(field(fieldName)); + } + + /** + * Creates an expression that removes leading and trailing whitespace from a string expression. + * + * @param string The expression representing the string to trim. + * @return A new {@link Expression} representing the trimmed string. + */ + @BetaApi + public static Expression trim(Expression string) { + return new FunctionExpression("trim", ImmutableList.of(string)); + } + + /** + * Creates an expression that removes leading and trailing whitespace from a string field. + * + * @param fieldName The name of the field containing the string to trim. + * @return A new {@link Expression} representing the trimmed string. + */ + @BetaApi + public static Expression trim(String fieldName) { + return trim(field(fieldName)); + } + + /** + * Creates an expression that removes specified characters from the beginning and end of a string + * or blob. + * + * @param value The expression representing the string or blob to trim. + * @param characters The characters to remove. + * @return A new {@link Expression} representing the trimmed string or blob. + */ + @BetaApi + public static Expression trimValue(Expression value, String characters) { + return new FunctionExpression("trim", ImmutableList.of(value, constant(characters))); + } + + /** + * Creates an expression that removes specified characters from the beginning and end of a string + * or blob. + * + * @param fieldName The name of the field containing the string or blob to trim. + * @param characters The characters to remove. + * @return A new {@link Expression} representing the trimmed string or blob. + */ + @BetaApi + public static Expression trimValue(String fieldName, String characters) { + return trimValue(field(fieldName), characters); + } + + /** + * Creates an expression that removes specified characters from the beginning and end of a string + * or blob. + * + * @param value The expression representing the string or blob to trim. + * @param characters The expression representing the characters to remove. + * @return A new {@link Expression} representing the trimmed string or blob. + */ + @BetaApi + public static Expression trimValue(Expression value, Expression characters) { + return new FunctionExpression("trim", ImmutableList.of(value, characters)); + } + + /** + * Creates an expression that removes specified characters from the beginning and end of a string + * or blob. + * + * @param fieldName The name of the field containing the string or blob to trim. + * @param characters The expression representing the characters to remove. + * @return A new {@link Expression} representing the trimmed string or blob. + */ + @BetaApi + public static Expression trimValue(String fieldName, Expression characters) { + return trimValue(field(fieldName), characters); + } + + /** + * Creates an expression that splits a string or blob by a delimiter. + * + * @param value The expression representing the string or blob to split. + * @param delimiter The delimiter to split by. + * @return A new {@link Expression} representing the split string or blob as an array. + */ + @BetaApi + public static Expression split(Expression value, Expression delimiter) { + return new FunctionExpression("split", ImmutableList.of(value, delimiter)); + } + + /** + * Creates an expression that splits a string or blob by a delimiter. + * + * @param value The expression representing the string or blob to split. + * @param delimiter The delimiter to split by. + * @return A new {@link Expression} representing the split string or blob as an array. + */ + @BetaApi + public static Expression split(Expression value, String delimiter) { + return split(value, constant(delimiter)); + } + + /** + * Creates an expression that splits a string or blob by a delimiter. + * + * @param fieldName The name of the field containing the string or blob to split. + * @param delimiter The delimiter to split by. + * @return A new {@link Expression} representing the split string or blob as an array. + */ + @BetaApi + public static Expression split(String fieldName, Expression delimiter) { + return split(field(fieldName), delimiter); + } + + /** + * Creates an expression that splits a string or blob by a delimiter. + * + * @param fieldName The name of the field containing the string or blob to split. + * @param delimiter The delimiter to split by. + * @return A new {@link Expression} representing the split string or blob as an array. + */ + @BetaApi + public static Expression split(String fieldName, String delimiter) { + return split(field(fieldName), constant(delimiter)); + } + + /** + * Creates an expression that concatenates string expressions together. + * + * @param firstString The expression representing the initial string value. + * @param otherStrings Optional additional string expressions or string constants to concatenate. + * @return A new {@link Expression} representing the concatenated string. + */ + @BetaApi + public static Expression stringConcat(Expression firstString, Object... otherStrings) { + ImmutableList.Builder builder = ImmutableList.builder(); + builder.add(firstString); + builder.addAll(toArrayOfExprOrConstant(otherStrings)); + return new FunctionExpression("string_concat", builder.build()); + } + + /** + * Creates an expression that concatenates string expressions together. + * + * @param fieldName The field name containing the initial string value. + * @param otherStrings Optional additional string expressions or string constants to concatenate. + * @return A new {@link Expression} representing the concatenated string. + */ + @BetaApi + public static Expression stringConcat(String fieldName, Object... otherStrings) { + return stringConcat(field(fieldName), otherStrings); + } + + /** + * Creates an expression that concatenates expressions together. + * + * @param first The expression representing the initial value. + * @param others Optional additional expressions or constants to concatenate. + * @return A new {@link Expression} representing the concatenated value. + */ + @BetaApi + public static Expression concat(Expression first, Object... others) { + ImmutableList.Builder builder = ImmutableList.builder(); + builder.add(first); + builder.addAll(toArrayOfExprOrConstant(others)); + return new FunctionExpression("concat", builder.build()); + } + + /** + * Creates an expression that concatenates expressions together. + * + * @param fieldName The field name containing the initial value. + * @param others Optional additional expressions or constants to concatenate. + * @return A new {@link Expression} representing the concatenated value. + */ + @BetaApi + public static Expression concat(String fieldName, Object... others) { + return concat(field(fieldName), others); + } + + // Map Functions + /** + * Creates an expression that creates a Firestore map value from an input object. + * + * @param elements The input map to evaluate in the expression. + * @return A new {@link Expression} representing the map function. + */ + @BetaApi + public static Expression map(Map elements) { + ImmutableList params = + elements.entrySet().stream() + .flatMap( + e -> Arrays.asList(constant(e.getKey()), toExprOrConstant(e.getValue())).stream()) + .collect(ImmutableList.toImmutableList()); + return new FunctionExpression("map", params); + } + + /** + * Accesses a value from a map (object) field using the provided {@code keyExpression}. + * + * @param map The expression representing the map. + * @param key The key to access in the map. + * @return A new {@link Expression} representing the value associated with the given key in the + * map. + */ + @BetaApi + public static Expression mapGet(Expression map, Expression key) { + return new FunctionExpression("map_get", ImmutableList.of(map, key)); + } + + /** + * Accesses a value from a map (object) field using the provided {@code key}. + * + * @param map The expression representing the map. + * @param key The key to access in the map. + * @return A new {@link Expression} representing the value associated with the given key in the + * map. + */ + @BetaApi + public static Expression mapGet(Expression map, String key) { + return mapGet(map, constant(key)); + } + + /** + * Accesses a value from a map (object) field using the provided {@code key}. + * + * @param fieldName The field name of the map field. + * @param key The key to access in the map. + * @return A new {@link Expression} representing the value associated with the given key in the + * map. + */ + @BetaApi + public static Expression mapGet(String fieldName, String key) { + return mapGet(field(fieldName), constant(key)); + } + + /** + * Accesses a value from a map (object) field using the provided {@code keyExpression}. + * + * @param fieldName The field name of the map field. + * @param key The key to access in the map. + * @return A new {@link Expression} representing the value associated with the given key in the + * map. + */ + @BetaApi + public static Expression mapGet(String fieldName, Expression key) { + return mapGet(field(fieldName), key); + } + + @BetaApi + public static Expression mapMerge(Expression firstMap, Expression secondMap) { + return mapMerge(firstMap, secondMap, new Expression[0]); + } + + @BetaApi + public static Expression mapMerge(String firstMapFieldName, Expression secondMap) { + return mapMerge(field(firstMapFieldName), secondMap, new Expression[0]); + } + + /** + * Creates an expression that merges multiple maps into a single map. If multiple maps have the + * same key, the later value is used. + * + * @param firstMap First map expression that will be merged. + * @param secondMap Second map expression that will be merged. + * @param otherMaps Additional maps to merge. + * @return A new {@link Expression} representing the mapMerge operation. + */ + @BetaApi + public static Expression mapMerge( + Expression firstMap, Expression secondMap, Expression... otherMaps) { + ImmutableList.Builder builder = ImmutableList.builder(); + builder.add(firstMap); + builder.add(secondMap); + builder.add(otherMaps); + return new FunctionExpression("map_merge", builder.build()); + } + + /** + * Creates an expression that merges multiple maps into a single map. If multiple maps have the + * same key, the later value is used. + * + * @param firstMapFieldName Field name of the first map expression that will be merged. + * @param secondMap Second map expression that will be merged. + * @param otherMaps Additional maps to merge. + * @return A new {@link Expression} representing the mapMerge operation. + */ + @BetaApi + public static Expression mapMerge( + String firstMapFieldName, Expression secondMap, Expression... otherMaps) { + return mapMerge(field(firstMapFieldName), secondMap, otherMaps); + } + + /** + * Creates an expression that removes a key from a map. + * + * @param mapExpr The expression representing the map. + * @param key The key to remove from the map. + * @return A new {@link Expression} representing the map with the key removed. + */ + @BetaApi + public static Expression mapRemove(Expression mapExpr, Expression key) { + return new FunctionExpression("map_remove", ImmutableList.of(mapExpr, key)); + } + + /** + * Creates an expression that removes a key from a map. + * + * @param mapField The field name of the map. + * @param key The key to remove from the map. + * @return A new {@link Expression} representing the map with the key removed. + */ + @BetaApi + public static Expression mapRemove(String mapField, Expression key) { + return mapRemove(field(mapField), key); + } + + /** + * Creates an expression that removes a key from a map. + * + * @param mapExpr The expression representing the map. + * @param key The key to remove from the map. + * @return A new {@link Expression} representing the map with the key removed. + */ + @BetaApi + public static Expression mapRemove(Expression mapExpr, String key) { + return mapRemove(mapExpr, constant(key)); + } + + /** + * Creates an expression that removes a key from a map. + * + * @param mapField The field name of the map. + * @param key The key to remove from the map. + * @return A new {@link Expression} representing the map with the key removed. + */ + @BetaApi + public static Expression mapRemove(String mapField, String key) { + return mapRemove(field(mapField), key); + } + + /** + * Creates an expression that reverses a string, blob, or array. + * + * @param expr An expression evaluating to a string, blob, or array value, which will be reversed. + * @return A new {@link Expression} representing the reversed value. + */ + @BetaApi + public static Expression reverse(Expression expr) { + return new FunctionExpression("reverse", ImmutableList.of(expr)); + } + + /** + * Creates an expression that reverses the field value, which must be a string, blob, or array. + * + * @param fieldName A field evaluating to a string, blob, or array value. + * @return A new {@link Expression} representing the reversed value. + */ + @BetaApi + public static Expression reverse(String fieldName) { + return reverse(field(fieldName)); + } + + // Array Functions + /** + * Creates an expression that creates a Firestore array value from an input object. + * + * @param elements The input elements to evaluate in the expression. + * @return A new {@link Expression} representing the array function. + */ + @BetaApi + public static Expression array(Object... elements) { + return new FunctionExpression("array", toArrayOfExprOrConstant(elements)); + } + + /** + * Creates an expression that creates a Firestore array value from an input object. + * + * @param elements The input elements to evaluate in the expression. + * @return A new {@link Expression} representing the array function. + */ + @BetaApi + public static Expression array(List elements) { + return new FunctionExpression("array", toArrayOfExprOrConstant(elements.toArray())); + } + + /** + * Creates an expression that concatenates multiple arrays into a single array. + * + * @param firstArray The first array expression to concatenate. + * @param otherArrays Additional arrays to concatenate. + * @return A new {@link Expression} representing the concatenated array. + */ + @BetaApi + public static Expression arrayConcat(Expression firstArray, Object... otherArrays) { + ImmutableList.Builder builder = ImmutableList.builder(); + builder.add(firstArray); + builder.addAll(toArrayOfExprOrConstant(otherArrays)); + return new FunctionExpression("array_concat", builder.build()); + } + + /** + * Creates an expression that concatenates multiple arrays into a single array. + * + * @param firstArrayField The field name of the first array to concatenate. + * @param otherArrays Additional arrays to concatenate. + * @return A new {@link Expression} representing the concatenated array. + */ + @BetaApi + public static Expression arrayConcat(String firstArrayField, Object... otherArrays) { + return arrayConcat(field(firstArrayField), otherArrays); + } + + /** + * Creates an expression that reverses an array. + * + * @param array The expression representing the array to reverse. + * @return A new {@link Expression} representing the reversed array. + */ + @BetaApi + public static Expression arrayReverse(Expression array) { + return new FunctionExpression("array_reverse", ImmutableList.of(array)); + } + + /** + * Creates an expression that reverses an array. + * + * @param arrayFieldName The field name of the array to reverse. + * @return A new {@link Expression} representing the reversed array. + */ + @BetaApi + public static Expression arrayReverse(String arrayFieldName) { + return arrayReverse(field(arrayFieldName)); + } + + /** + * Creates an expression that checks if an array contains a specified element. + * + * @param array The expression representing the array. + * @param element The element to check for. + * @return A new {@link BooleanExpression} representing the array contains comparison. + */ + @BetaApi + public static BooleanExpression arrayContains(Expression array, Expression element) { + return new BooleanExpression("array_contains", array, element); + } + + /** + * Creates an expression that checks if an array contains a specified element. + * + * @param arrayFieldName The field name of the array. + * @param element The element to check for. + * @return A new {@link BooleanExpression} representing the array contains comparison. + */ + @BetaApi + public static BooleanExpression arrayContains(String arrayFieldName, Expression element) { + return arrayContains(field(arrayFieldName), element); + } + + /** + * Creates an expression that checks if an array contains a specified element. + * + * @param array The expression representing the array. + * @param element The element to check for. + * @return A new {@link BooleanExpression} representing the array contains comparison. + */ + @BetaApi + public static BooleanExpression arrayContains(Expression array, Object element) { + return arrayContains(array, toExprOrConstant(element)); + } + + /** + * Creates an expression that checks if an array contains a specified element. + * + * @param arrayFieldName The field name of the array. + * @param element The element to check for. + * @return A new {@link BooleanExpression} representing the array contains comparison. + */ + @BetaApi + public static BooleanExpression arrayContains(String arrayFieldName, Object element) { + return arrayContains(field(arrayFieldName), toExprOrConstant(element)); + } + + /** + * Creates an expression that checks if an array contains all of the provided values. + * + * @param array The expression representing the array. + * @param values The values to check for. + * @return A new {@link BooleanExpression} representing the array contains all comparison. + */ + @BetaApi + public static BooleanExpression arrayContainsAll(Expression array, List values) { + return arrayContainsAll(array, array(values)); + } + + /** + * Creates an expression that checks if an array contains all of the elements of another array. + * + * @param array The expression representing the array. + * @param arrayExpression The expression representing the array of values to check for. + * @return A new {@link BooleanExpression} representing the array contains all comparison. + */ + @BetaApi + public static BooleanExpression arrayContainsAll(Expression array, Expression arrayExpression) { + return new BooleanExpression("array_contains_all", array, arrayExpression); + } + + /** + * Creates an expression that checks if an array contains all of the provided values. + * + * @param arrayFieldName The field name of the array. + * @param values The values to check for. + * @return A new {@link BooleanExpression} representing the array contains all comparison. + */ + @BetaApi + public static BooleanExpression arrayContainsAll(String arrayFieldName, List values) { + return arrayContainsAll(field(arrayFieldName), array(values)); + } + + /** + * Creates an expression that checks if an array contains all of the elements of another array. + * + * @param arrayFieldName The field name of the array. + * @param arrayExpression The expression representing the array of values to check for. + * @return A new {@link BooleanExpression} representing the array contains all comparison. + */ + @BetaApi + public static BooleanExpression arrayContainsAll( + String arrayFieldName, Expression arrayExpression) { + return arrayContainsAll(field(arrayFieldName), arrayExpression); + } + + /** + * Creates an expression that checks if an array contains any of the provided values. + * + * @param array The expression representing the array. + * @param values The values to check for. + * @return A new {@link BooleanExpression} representing the array contains any comparison. + */ + @BetaApi + public static BooleanExpression arrayContainsAny(Expression array, List values) { + return new BooleanExpression("array_contains_any", array, array(values)); + } + + /** + * Creates an expression that checks if an array contains any of the elements of another array. + * + * @param array The expression representing the array. + * @param arrayExpression The expression representing the array of values to check for. + * @return A new {@link BooleanExpression} representing the array contains any comparison. + */ + @BetaApi + public static BooleanExpression arrayContainsAny(Expression array, Expression arrayExpression) { + return new BooleanExpression("array_contains_any", array, arrayExpression); + } + + /** + * Creates an expression that checks if an array contains any of the provided values. + * + * @param arrayFieldName The field name of the array. + * @param values The values to check for. + * @return A new {@link BooleanExpression} representing the array contains any comparison. + */ + @BetaApi + public static BooleanExpression arrayContainsAny(String arrayFieldName, List values) { + return arrayContainsAny(field(arrayFieldName), array(values)); + } + + /** + * Creates an expression that checks if an array contains any of the elements of another array. + * + * @param arrayFieldName The field name of the array. + * @param arrayExpression The expression representing the array of values to check for. + * @return A new {@link BooleanExpression} representing the array contains any comparison. + */ + @BetaApi + public static BooleanExpression arrayContainsAny( + String arrayFieldName, Expression arrayExpression) { + return arrayContainsAny(field(arrayFieldName), arrayExpression); + } + + /** + * Creates an expression that returns the length of an array. + * + * @param array The expression representing the array. + * @return A new {@link Expression} representing the length of the array. + */ + @BetaApi + public static Expression arrayLength(Expression array) { + return new FunctionExpression("array_length", ImmutableList.of(array)); + } + + /** + * Creates an expression that returns the length of an array. + * + * @param arrayFieldName The field name of the array. + * @return A new {@link Expression} representing the length of the array. + */ + @BetaApi + public static Expression arrayLength(String arrayFieldName) { + return arrayLength(field(arrayFieldName)); + } + + /** + * Creates an expression that returns an element from an array at a specified index. + * + * @param array The expression representing the array. + * @param offset The index of the element to return. + * @return A new {@link Expression} representing the element at the specified index. + */ + @BetaApi + public static Expression arrayGet(Expression array, Expression offset) { + return new FunctionExpression("array_get", ImmutableList.of(array, offset)); + } + + /** + * Creates an expression that returns an element from an array at a specified index. + * + * @param array The expression representing the array. + * @param offset The index of the element to return. + * @return A new {@link Expression} representing the element at the specified index. + */ + @BetaApi + public static Expression arrayGet(Expression array, int offset) { + return arrayGet(array, constant(offset)); + } + + /** + * Creates an expression that returns an element from an array at a specified index. + * + * @param arrayFieldName The field name of the array. + * @param offset The index of the element to return. + * @return A new {@link Expression} representing the element at the specified index. + */ + @BetaApi + public static Expression arrayGet(String arrayFieldName, Expression offset) { + return arrayGet(field(arrayFieldName), offset); + } + + /** + * Creates an expression that returns an element from an array at a specified index. + * + * @param arrayFieldName The field name of the array. + * @param offset The index of the element to return. + * @return A new {@link Expression} representing the element at the specified index. + */ + @BetaApi + public static Expression arrayGet(String arrayFieldName, int offset) { + return arrayGet(field(arrayFieldName), constant(offset)); + } + + /** + * Creates an expression that returns the sum of the elements of an array. + * + * @param array The expression representing the array. + * @return A new {@link Expression} representing the sum of the elements of the array. + */ + @BetaApi + public static Expression arraySum(Expression array) { + return new FunctionExpression("sum", ImmutableList.of(array)); + } + + /** + * Creates an expression that returns the sum of the elements of an array. + * + * @param arrayFieldName The field name of the array. + * @return A new {@link Expression} representing the sum of the elements of the array. + */ + @BetaApi + public static Expression arraySum(String arrayFieldName) { + return arraySum(field(arrayFieldName)); + } + + // Vector Functions + /** + * Creates an expression that calculates the cosine distance between two vectors. + * + * @param vector1 The first vector. + * @param vector2 The second vector. + * @return A new {@link Expression} representing the cosine distance. + */ + @BetaApi + public static Expression cosineDistance(Expression vector1, Expression vector2) { + return new FunctionExpression("cosine_distance", ImmutableList.of(vector1, vector2)); + } + + /** + * Creates an expression that calculates the cosine distance between two vectors. + * + * @param vector1 The first vector. + * @param vector2 The second vector. + * @return A new {@link Expression} representing the cosine distance. + */ + @BetaApi + public static Expression cosineDistance(Expression vector1, double[] vector2) { + return cosineDistance(vector1, vector(vector2)); + } + + /** + * Creates an expression that calculates the cosine distance between two vectors. + * + * @param vectorFieldName The field name of the first vector. + * @param vector The second vector. + * @return A new {@link Expression} representing the cosine distance. + */ + @BetaApi + public static Expression cosineDistance(String vectorFieldName, Expression vector) { + return cosineDistance(field(vectorFieldName), vector); + } + + /** + * Creates an expression that calculates the cosine distance between two vectors. + * + * @param vectorFieldName The field name of the first vector. + * @param vector The second vector. + * @return A new {@link Expression} representing the cosine distance. + */ + @BetaApi + public static Expression cosineDistance(String vectorFieldName, double[] vector) { + return cosineDistance(field(vectorFieldName), vector(vector)); + } + + /** + * Creates an expression that calculates the dot product of two vectors. + * + * @param vector1 The first vector. + * @param vector2 The second vector. + * @return A new {@link Expression} representing the dot product. + */ + @BetaApi + public static Expression dotProduct(Expression vector1, Expression vector2) { + return new FunctionExpression("dot_product", ImmutableList.of(vector1, vector2)); + } + + /** + * Creates an expression that calculates the dot product of two vectors. + * + * @param vector1 The first vector. + * @param vector2 The second vector. + * @return A new {@link Expression} representing the dot product. + */ + @BetaApi + public static Expression dotProduct(Expression vector1, double[] vector2) { + return dotProduct(vector1, vector(vector2)); + } + + /** + * Creates an expression that calculates the dot product of two vectors. + * + * @param vectorFieldName The field name of the first vector. + * @param vector The second vector. + * @return A new {@link Expression} representing the dot product. + */ + @BetaApi + public static Expression dotProduct(String vectorFieldName, Expression vector) { + return dotProduct(field(vectorFieldName), vector); + } + + /** + * Creates an expression that calculates the dot product of two vectors. + * + * @param vectorFieldName The field name of the first vector. + * @param vector The second vector. + * @return A new {@link Expression} representing the dot product. + */ + @BetaApi + public static Expression dotProduct(String vectorFieldName, double[] vector) { + return dotProduct(field(vectorFieldName), vector(vector)); + } + + /** + * Creates an expression that calculates the Euclidean distance between two vectors. + * + * @param vector1 The first vector. + * @param vector2 The second vector. + * @return A new {@link Expression} representing the Euclidean distance. + */ + @BetaApi + public static Expression euclideanDistance(Expression vector1, Expression vector2) { + return new FunctionExpression("euclidean_distance", ImmutableList.of(vector1, vector2)); + } + + /** + * Creates an expression that calculates the Euclidean distance between two vectors. + * + * @param vector1 The first vector. + * @param vector2 The second vector. + * @return A new {@link Expression} representing the Euclidean distance. + */ + @BetaApi + public static Expression euclideanDistance(Expression vector1, double[] vector2) { + return euclideanDistance(vector1, vector(vector2)); + } + + /** + * Creates an expression that calculates the Euclidean distance between two vectors. + * + * @param vectorFieldName The field name of the first vector. + * @param vector The second vector. + * @return A new {@link Expression} representing the Euclidean distance. + */ + @BetaApi + public static Expression euclideanDistance(String vectorFieldName, Expression vector) { + return euclideanDistance(field(vectorFieldName), vector); + } + + /** + * Creates an expression that calculates the Euclidean distance between two vectors. + * + * @param vectorFieldName The field name of the first vector. + * @param vector The second vector. + * @return A new {@link Expression} representing the Euclidean distance. + */ + @BetaApi + public static Expression euclideanDistance(String vectorFieldName, double[] vector) { + return euclideanDistance(field(vectorFieldName), vector(vector)); + } + + /** + * Creates an expression that calculates the length of a vector. + * + * @param vectorExpression The expression representing the vector. + * @return A new {@link Expression} representing the length of the vector. + */ + @BetaApi + public static Expression vectorLength(Expression vectorExpression) { + return new FunctionExpression("vector_length", ImmutableList.of(vectorExpression)); + } + + /** + * Creates an expression that calculates the length of a vector. + * + * @param fieldName The field name of the vector. + * @return A new {@link Expression} representing the length of the vector. + */ + @BetaApi + public static Expression vectorLength(String fieldName) { + return vectorLength(field(fieldName)); + } + + // Timestamp Functions + /** + * Creates an expression that converts a Unix timestamp in microseconds to a Firestore timestamp. + * + * @param expr The expression representing the Unix timestamp in microseconds. + * @return A new {@link Expression} representing the Firestore timestamp. + */ + @BetaApi + public static Expression unixMicrosToTimestamp(Expression expr) { + return new FunctionExpression("unix_micros_to_timestamp", ImmutableList.of(expr)); + } + + /** + * Creates an expression that interprets a field's value as the number of microseconds since the + * Unix epoch (1970-01-01 00:00:00 UTC) and returns a timestamp. + * + * @param fieldName The name of the field containing the number of microseconds since epoch. + * @return A new {@link Expression} representing the timestamp. + */ + @BetaApi + public static Expression unixMicrosToTimestamp(String fieldName) { + return unixMicrosToTimestamp(field(fieldName)); + } + + /** + * Creates an expression that converts a timestamp expression to the number of microseconds since + * the Unix epoch (1970-01-01 00:00:00 UTC). + * + * @param expr The expression representing the timestamp. + * @return A new {@link Expression} representing the number of microseconds since epoch. + */ + @BetaApi + public static Expression timestampToUnixMicros(Expression expr) { + return new FunctionExpression("timestamp_to_unix_micros", ImmutableList.of(expr)); + } + + /** + * Creates an expression that converts a timestamp field to the number of microseconds since the + * Unix epoch (1970-01-01 00:00:00 UTC). + * + * @param fieldName The name of the field that contains the timestamp. + * @return A new {@link Expression} representing the number of microseconds since epoch. + */ + @BetaApi + public static Expression timestampToUnixMicros(String fieldName) { + return timestampToUnixMicros(field(fieldName)); + } + + /** + * Creates an expression that interprets an expression as the number of milliseconds since the + * Unix epoch (1970-01-01 00:00:00 UTC) and returns a timestamp. + * + * @param expr The expression representing the number of milliseconds since epoch. + * @return A new {@link Expression} representing the timestamp. + */ + @BetaApi + public static Expression unixMillisToTimestamp(Expression expr) { + return new FunctionExpression("unix_millis_to_timestamp", ImmutableList.of(expr)); + } + + /** + * Creates an expression that interprets a field's value as the number of milliseconds since the + * Unix epoch (1970-01-01 00:00:00 UTC) and returns a timestamp. + * + * @param fieldName The name of the field containing the number of milliseconds since epoch. + * @return A new {@link Expression} representing the timestamp. + */ + @BetaApi + public static Expression unixMillisToTimestamp(String fieldName) { + return unixMillisToTimestamp(field(fieldName)); + } + + /** + * Creates an expression that converts a timestamp expression to the number of milliseconds since + * the Unix epoch (1970-01-01 00:00:00 UTC). + * + * @param expr The expression representing the timestamp. + * @return A new {@link Expression} representing the number of milliseconds since epoch. + */ + @BetaApi + public static Expression timestampToUnixMillis(Expression expr) { + return new FunctionExpression("timestamp_to_unix_millis", ImmutableList.of(expr)); + } + + /** + * Creates an expression that converts a timestamp field to the number of milliseconds since the + * Unix epoch (1970-01-01 00:00:00 UTC). + * + * @param fieldName The name of the field that contains the timestamp. + * @return A new {@link Expression} representing the number of milliseconds since epoch. + */ + @BetaApi + public static Expression timestampToUnixMillis(String fieldName) { + return timestampToUnixMillis(field(fieldName)); + } + + /** + * Creates an expression that interprets an expression as the number of seconds since the Unix + * epoch (1970-01-01 00:00:00 UTC) and returns a timestamp. + * + * @param expr The expression representing the number of seconds since epoch. + * @return A new {@link Expression} representing the timestamp. + */ + @BetaApi + public static Expression unixSecondsToTimestamp(Expression expr) { + return new FunctionExpression("unix_seconds_to_timestamp", ImmutableList.of(expr)); + } + + /** + * Creates an expression that interprets a field's value as the number of seconds since the Unix + * epoch (1970-01-01 00:00:00 UTC) and returns a timestamp. + * + * @param fieldName The name of the field containing the number of seconds since epoch. + * @return A new {@link Expression} representing the timestamp. + */ + @BetaApi + public static Expression unixSecondsToTimestamp(String fieldName) { + return unixSecondsToTimestamp(field(fieldName)); + } + + /** + * Creates an expression that converts a timestamp expression to the number of seconds since the + * Unix epoch (1970-01-01 00:00:00 UTC). + * + * @param expr The expression representing the timestamp. + * @return A new {@link Expression} representing the number of seconds since epoch. + */ + @BetaApi + public static Expression timestampToUnixSeconds(Expression expr) { + return new FunctionExpression("timestamp_to_unix_seconds", ImmutableList.of(expr)); + } + + /** + * Creates an expression that converts a timestamp field to the number of seconds since the Unix + * epoch (1970-01-01 00:00:00 UTC). + * + * @param fieldName The name of the field that contains the timestamp. + * @return A new {@link Expression} representing the number of seconds since epoch. + */ + @BetaApi + public static Expression timestampToUnixSeconds(String fieldName) { + return timestampToUnixSeconds(field(fieldName)); + } + + /** + * Creates an expression that adds a specified amount of time to a timestamp. + * + * @param timestamp The expression representing the timestamp. + * @param unit The expression representing the unit of time to add. Valid units include + * "microsecond", "millisecond", "second", "minute", "hour" and "day". + * @param amount The expression representing the amount of time to add. + * @return A new {@link Expression} representing the resulting timestamp. + */ + @BetaApi + public static Expression timestampAdd(Expression timestamp, Expression unit, Expression amount) { + return new FunctionExpression("timestamp_add", ImmutableList.of(timestamp, unit, amount)); + } + + /** + * Creates an expression that adds a specified amount of time to a timestamp. + * + * @param timestamp The expression representing the timestamp. + * @param unit The unit of time to add. Valid units include "microsecond", "millisecond", + * "second", "minute", "hour" and "day". + * @param amount The amount of time to add. + * @return A new {@link Expression} representing the resulting timestamp. + */ + @BetaApi + public static Expression timestampAdd(Expression timestamp, String unit, long amount) { + return timestampAdd(timestamp, constant(unit), constant(amount)); + } + + /** + * Creates an expression that adds a specified amount of time to a timestamp. + * + * @param fieldName The name of the field that contains the timestamp. + * @param unit The expression representing the unit of time to add. Valid units include + * "microsecond", "millisecond", "second", "minute", "hour" and "day". + * @param amount The expression representing the amount of time to add. + * @return A new {@link Expression} representing the resulting timestamp. + */ + @BetaApi + public static Expression timestampAdd(String fieldName, Expression unit, Expression amount) { + return timestampAdd(field(fieldName), unit, amount); + } + + /** + * Creates an expression that adds a specified amount of time to a timestamp. + * + * @param fieldName The name of the field that contains the timestamp. + * @param unit The unit of time to add. Valid units include "microsecond", "millisecond", + * "second", "minute", "hour" and "day". + * @param amount The amount of time to add. + * @return A new {@link Expression} representing the resulting timestamp. + */ + @BetaApi + public static Expression timestampAdd(String fieldName, String unit, long amount) { + return timestampAdd(field(fieldName), constant(unit), constant(amount)); + } + + /** + * Creates an expression that subtracts a specified amount of time to a timestamp. + * + * @param timestamp The expression representing the timestamp. + * @param unit The expression representing the unit of time to subtract. Valid units include + * "microsecond", "millisecond", "second", "minute", "hour" and "day". + * @param amount The expression representing the amount of time to subtract. + * @return A new {@link Expression} representing the resulting timestamp. + */ + @BetaApi + public static Expression timestampSubtract( + Expression timestamp, Expression unit, Expression amount) { + return new FunctionExpression("timestamp_subtract", ImmutableList.of(timestamp, unit, amount)); + } + + /** + * Creates an expression that subtracts a specified amount of time to a timestamp. + * + * @param timestamp The expression representing the timestamp. + * @param unit The unit of time to subtract. Valid units include "microsecond", "millisecond", + * "second", "minute", "hour" and "day". + * @param amount The amount of time to subtract. + * @return A new {@link Expression} representing the resulting timestamp. + */ + @BetaApi + public static Expression timestampSubtract(Expression timestamp, String unit, long amount) { + return timestampSubtract(timestamp, constant(unit), constant(amount)); + } + + /** + * Creates an expression that subtracts a specified amount of time to a timestamp. + * + * @param fieldName The name of the field that contains the timestamp. + * @param unit The unit of time to subtract. Valid units include "microsecond", "millisecond", + * "second", "minute", "hour" and "day". + * @param amount The amount of time to subtract. + * @return A new {@link Expression} representing the resulting timestamp. + */ + @BetaApi + public static Expression timestampSubtract(String fieldName, Expression unit, Expression amount) { + return timestampSubtract(field(fieldName), unit, amount); + } + + /** + * Creates an expression that subtracts a specified amount of time to a timestamp. + * + * @param fieldName The name of the field that contains the timestamp. + * @param unit The unit of time to subtract. Valid units include "microsecond", "millisecond", + * "second", "minute", "hour" and "day". + * @param amount The amount of time to subtract. + * @return A new {@link Expression} representing the resulting timestamp. + */ + @BetaApi + public static Expression timestampSubtract(String fieldName, String unit, long amount) { + return timestampSubtract(field(fieldName), constant(unit), constant(amount)); + } + + /** + * Creates an expression that truncates a timestamp to a specified granularity. + * + * @param timestamp The timestamp expression. + * @param granularity The granularity to truncate to. Valid values are "microsecond", + * "millisecond", "second", "minute", "hour", "day", "week", "week(monday)", "week(tuesday)", + * "week(wednesday)", "week(thursday)", "week(friday)", "week(saturday)", "week(sunday)", + * "isoweek", "month", "quarter", "year", and "isoyear". + * @return A new {@link Expression} representing the truncated timestamp. + */ + @BetaApi + public static Expression timestampTruncate(Expression timestamp, String granularity) { + return new FunctionExpression( + "timestamp_trunc", ImmutableList.of(timestamp, constant(granularity))); + } + + /** + * Creates an expression that truncates a timestamp to a specified granularity. + * + * @param timestamp The timestamp expression. + * @param granularity The granularity expression to truncate to. Valid values are "microsecond", + * "millisecond", "second", "minute", "hour", "day", "week", "week(monday)", "week(tuesday)", + * "week(wednesday)", "week(thursday)", "week(friday)", "week(saturday)", "week(sunday)", + * "isoweek", "month", "quarter", "year", and "isoyear". + * @return A new {@link Expression} representing the truncated timestamp. + */ + @BetaApi + public static Expression timestampTruncate(Expression timestamp, Expression granularity) { + return new FunctionExpression("timestamp_trunc", ImmutableList.of(timestamp, granularity)); + } + + /** + * Creates an expression that truncates a timestamp to a specified granularity. + * + * @param fieldName The name of the field containing the timestamp. + * @param granularity The granularity to truncate to. Valid values are "microsecond", + * "millisecond", "second", "minute", "hour", "day", "week", "week(monday)", "week(tuesday)", + * "week(wednesday)", "week(thursday)", "week(friday)", "week(saturday)", "week(sunday)", + * "isoweek", "month", "quarter", "year", and "isoyear". + * @return A new {@link Expression} representing the truncated timestamp. + */ + @BetaApi + public static Expression timestampTruncate(String fieldName, String granularity) { + return timestampTruncate(field(fieldName), constant(granularity)); + } + + /** + * Creates an expression that truncates a timestamp to a specified granularity. + * + * @param fieldName The name of the field containing the timestamp. + * @param granularity The granularity expression to truncate to. Valid values are "microsecond", + * "millisecond", "second", "minute", "hour", "day", "week", "week(monday)", "week(tuesday)", + * "week(wednesday)", "week(thursday)", "week(friday)", "week(saturday)", "week(sunday)", + * "isoweek", "month", "quarter", "year", and "isoyear". + * @return A new {@link Expression} representing the truncated timestamp. + */ + @BetaApi + public static Expression timestampTruncate(String fieldName, Expression granularity) { + return timestampTruncate(field(fieldName), granularity); + } + + /** + * Creates an expression that truncates a timestamp to a specified granularity in a given + * timezone. + * + * @param timestamp The timestamp expression. + * @param granularity The granularity to truncate to. Valid values are "microsecond", + * "millisecond", "second", "minute", "hour", "day", "week", "week(monday)", "week(tuesday)", + * "week(wednesday)", "week(thursday)", "week(friday)", "week(saturday)", "week(sunday)", + * "isoweek", "month", "quarter", "year", and "isoyear". + * @param timezone The timezone to use for truncation. Valid values are from the TZ database + * (e.g., "America/Los_Angeles") or in the format "Etc/GMT-1". + * @return A new {@link Expression} representing the truncated timestamp. + */ + @BetaApi + public static Expression timestampTruncate( + Expression timestamp, String granularity, String timezone) { + return new FunctionExpression( + "timestamp_trunc", ImmutableList.of(timestamp, constant(granularity), constant(timezone))); + } + + /** + * Creates an expression that truncates a timestamp to a specified granularity in a given + * timezone. + * + * @param timestamp The timestamp expression. + * @param granularity The granularity expression to truncate to. Valid values are "microsecond", + * "millisecond", "second", "minute", "hour", "day", "week", "week(monday)", "week(tuesday)", + * "week(wednesday)", "week(thursday)", "week(friday)", "week(saturday)", "week(sunday)", + * "isoweek", "month", "quarter", "year", and "isoyear". + * @param timezone The timezone to use for truncation. Valid values are from the TZ database + * (e.g., "America/Los_Angeles") or in the format "Etc/GMT-1". + * @return A new {@link Expression} representing the truncated timestamp. + */ + @BetaApi + public static Expression timestampTruncate( + Expression timestamp, Expression granularity, String timezone) { + return new FunctionExpression( + "timestamp_trunc", ImmutableList.of(timestamp, granularity, constant(timezone))); + } + + /** + * Creates an expression that truncates a timestamp to a specified granularity in a given + * timezone. + * + * @param fieldName The name of the field containing the timestamp. + * @param granularity The granularity to truncate to. Valid values are "microsecond", + * "millisecond", "second", "minute", "hour", "day", "week", "week(monday)", "week(tuesday)", + * "week(wednesday)", "week(thursday)", "week(friday)", "week(saturday)", "week(sunday)", + * "isoweek", "month", "quarter", "year", and "isoyear". + * @param timezone The timezone to use for truncation. Valid values are from the TZ database + * (e.g., "America/Los_Angeles") or in the format "Etc/GMT-1". + * @return A new {@link Expression} representing the truncated timestamp. + */ + @BetaApi + public static Expression timestampTruncate( + String fieldName, String granularity, String timezone) { + return timestampTruncate(field(fieldName), constant(granularity), timezone); + } + + /** + * Creates an expression that truncates a timestamp to a specified granularity in a given + * timezone. + * + * @param fieldName The name of the field containing the timestamp. + * @param granularity The granularity expression to truncate to. Valid values are "microsecond", + * "millisecond", "second", "minute", "hour", "day", "week", "week(monday)", "week(tuesday)", + * "week(wednesday)", "week(thursday)", "week(friday)", "week(saturday)", "week(sunday)", + * "isoweek", "month", "quarter", "year", and "isoyear". + * @param timezone The timezone to use for truncation. Valid values are from the TZ database + * (e.g., "America/Los_Angeles") or in the format "Etc/GMT-1". + * @return A new {@link Expression} representing the truncated timestamp. + */ + @BetaApi + public static Expression timestampTruncate( + String fieldName, Expression granularity, String timezone) { + return timestampTruncate(field(fieldName), granularity, timezone); + } + + // Conditional Functions + /** + * Creates a conditional expression that evaluates to a {@code thenExpr} expression if a condition + * is true or an {@code elseExpr} expression if the condition is false. + * + * @param condition The condition to evaluate. + * @param thenExpr The expression to evaluate if the condition is true. + * @param elseExpr The expression to evaluate if the condition is false. + * @return A new {@link Expression} representing the conditional operation. + */ + @BetaApi + public static Expression conditional( + BooleanExpression condition, Expression thenExpr, Expression elseExpr) { + return new FunctionExpression("conditional", ImmutableList.of(condition, thenExpr, elseExpr)); + } + + /** + * Creates a conditional expression that evaluates to a {@code thenValue} if a condition is true + * or an {@code elseValue} if the condition is false. + * + * @param condition The condition to evaluate. + * @param thenValue Value if the condition is true. + * @param elseValue Value if the condition is false. + * @return A new {@link Expression} representing the conditional operation. + */ + @BetaApi + public static Expression conditional( + BooleanExpression condition, Object thenValue, Object elseValue) { + return conditional(condition, toExprOrConstant(thenValue), toExprOrConstant(elseValue)); + } + + // Error Handling Functions + /** + * Creates an expression that returns the {@code catchExpr} argument if there is an error, else + * return the result of the {@code tryExpr} argument evaluation. + * + * @param tryExpr The try expression. + * @param catchExpr The catch expression that will be evaluated and returned if the {@code + * tryExpr} produces an error. + * @return A new {@link Expression} representing the ifError operation. + */ + @BetaApi + public static Expression ifError(Expression tryExpr, Expression catchExpr) { + return new FunctionExpression("if_error", ImmutableList.of(tryExpr, catchExpr)); + } + + /** + * Creates an expression that returns the {@code catchExpr} argument if there is an error, else + * return the result of the {@code tryExpr} argument evaluation. + * + *

This overload will return {@link BooleanExpression} when both parameters are also {@link + * BooleanExpression}. + * + * @param tryExpr The try boolean expression. + * @param catchExpr The catch boolean expression that will be evaluated and returned if the {@code + * tryExpr} produces an error. + * @return A new {@link BooleanExpression} representing the ifError operation. + */ + @BetaApi + public static BooleanExpression ifError(BooleanExpression tryExpr, BooleanExpression catchExpr) { + return new BooleanExpression("if_error", tryExpr, catchExpr); + } + + /** + * Creates an expression that returns the {@code catchValue} argument if there is an error, else + * return the result of the {@code tryExpr} argument evaluation. + * + * @param tryExpr The try expression. + * @param catchValue The value that will be returned if the {@code tryExpr} produces an error. + * @return A new {@link Expression} representing the ifError operation. + */ + @BetaApi + public static Expression ifError(Expression tryExpr, Object catchValue) { + return ifError(tryExpr, toExprOrConstant(catchValue)); + } + + /** + * Creates an expression that checks if a given expression produces an error. + * + * @param expr The expression to check. + * @return A new {@link BooleanExpression} representing the `isError` check. + */ + @BetaApi + public static BooleanExpression isError(Expression expr) { + return new BooleanExpression("is_error", expr); + } + + // Other Utility Functions + /** + * Creates an expression that returns the document ID from a path. + * + * @param documentPath An expression the evaluates to document path. + * @return A new {@link Expression} representing the documentId operation. + */ + @BetaApi + public static Expression documentId(Expression documentPath) { + return new FunctionExpression("document_id", ImmutableList.of(documentPath)); + } + + /** + * Creates an expression that returns the document ID from a path. + * + * @param documentPath The string representation of the document path. + * @return A new {@link Expression} representing the documentId operation. + */ + @BetaApi + public static Expression documentId(String documentPath) { + return documentId(constant(documentPath)); + } + + /** + * Creates an expression that returns the document ID from a {@link DocumentReference}. + * + * @param docRef The {@link DocumentReference}. + * @return A new {@link Expression} representing the documentId operation. + */ + @BetaApi + public static Expression documentId(DocumentReference docRef) { + return documentId(constant(docRef)); + } + + /** + * Creates an expression that returns the collection ID from a path. + * + * @param path An expression the evaluates to document path. + * @return A new {@link Expression} representing the collectionId operation. + */ + @BetaApi + public static Expression collectionId(Expression path) { + return new FunctionExpression("collection_id", ImmutableList.of(path)); + } + + /** + * Creates an expression that returns the collection ID from a path. + * + * @param pathFieldName The field name of the path. + * @return A new {@link Expression} representing the collectionId operation. + */ + @BetaApi + public static Expression collectionId(String pathFieldName) { + return collectionId(field(pathFieldName)); + } + + // Type Checking Functions + /** + * Creates an expression that checks if a field exists. + * + * @param value An expression evaluates to the name of the field to check. + * @return A new {@link Expression} representing the exists check. + */ + @BetaApi + public static BooleanExpression exists(Expression value) { + return new BooleanExpression("exists", value); + } + + /** + * Creates an expression that checks if a field exists. + * + * @param fieldName The field name to check. + * @return A new {@link Expression} representing the exists check. + */ + @BetaApi + public static BooleanExpression exists(String fieldName) { + return exists(field(fieldName)); + } + + /** + * Creates an expression that returns true if a value is absent. Otherwise, returns false even if + * the value is null. + * + * @param value The expression to check. + * @return A new {@link BooleanExpression} representing the isAbsent operation. + */ + @BetaApi + public static BooleanExpression isAbsent(Expression value) { + return new BooleanExpression("is_absent", value); + } + + /** + * Creates an expression that returns true if a field is absent. Otherwise, returns false even if + * the field value is null. + * + * @param fieldName The field to check. + * @return A new {@link BooleanExpression} representing the isAbsent operation. + */ + @BetaApi + public static BooleanExpression isAbsent(String fieldName) { + return isAbsent(field(fieldName)); + } + + /** + * Creates an expression that checks if an expression evaluates to 'NaN' (Not a Number). + * + * @param value The expression to check. + * @return A new {@link BooleanExpression} representing the isNan operation. + */ + @BetaApi + public static BooleanExpression isNaN(Expression value) { + return new BooleanExpression("is_nan", value); + } + + /** + * Creates an expression that checks if a field's value evaluates to 'NaN' (Not a Number). + * + * @param fieldName The field to check. + * @return A new {@link BooleanExpression} representing the isNan operation. + */ + @BetaApi + public static BooleanExpression isNaN(String fieldName) { + return isNaN(field(fieldName)); + } + + /** + * Creates an expression that checks if the result of an expression is null. + * + * @param value The expression to check. + * @return A new {@link BooleanExpression} representing the isNull operation. + */ + @BetaApi + public static BooleanExpression isNull(Expression value) { + return new BooleanExpression("is_null", value); + } + + /** + * Creates an expression that checks if the value of a field is null. + * + * @param fieldName The field to check. + * @return A new {@link BooleanExpression} representing the isNull operation. + */ + @BetaApi + public static BooleanExpression isNull(String fieldName) { + return isNull(field(fieldName)); + } + + /** + * Creates an expression that checks if the result of an expression is not null. + * + * @param value The expression to check. + * @return A new {@link BooleanExpression} representing the isNotNull operation. + */ + @BetaApi + public static BooleanExpression isNotNull(Expression value) { + return new BooleanExpression("is_not_null", value); + } + + /** + * Creates an expression that checks if the value of a field is not null. + * + * @param fieldName The field to check. + * @return A new {@link BooleanExpression} representing the isNotNull operation. + */ + @BetaApi + public static BooleanExpression isNotNull(String fieldName) { + return isNotNull(field(fieldName)); + } + + /** + * Creates an expression that returns a string indicating the type of the value this expression + * evaluates to. + * + * @param expr The expression to get the type of. + * @return A new {@link Expression} representing the type operation. + */ + @BetaApi + public static Expression type(Expression expr) { + return new FunctionExpression("type", ImmutableList.of(expr)); + } + + /** + * Creates an expression that returns a string indicating the type of the value this field + * evaluates to. + * + * @param fieldName The name of the field to get the type of. + * @return A new {@link Expression} representing the type operation. + */ + @BetaApi + public static Expression type(String fieldName) { + return type(field(fieldName)); + } + + // Numeric Operations + /** + * Creates an expression that rounds {@code numericExpr} to nearest integer. + * + *

Rounds away from zero in halfway cases. + * + * @param numericExpr An expression that returns number when evaluated. + * @return A new {@link Expression} representing an integer result from the round operation. + */ + @BetaApi + public static Expression round(Expression numericExpr) { + return new FunctionExpression("round", ImmutableList.of(numericExpr)); + } + + /** + * Creates an expression that rounds {@code numericField} to nearest integer. + * + *

Rounds away from zero in halfway cases. + * + * @param numericField Name of field that returns number when evaluated. + * @return A new {@link Expression} representing an integer result from the round operation. + */ + @BetaApi + public static Expression round(String numericField) { + return round(field(numericField)); + } + + /** + * Creates an expression that rounds off {@code numericExpr} to {@code decimalPlace} decimal + * places if {@code decimalPlace} is positive, rounds off digits to the left of the decimal point + * if {@code decimalPlace} is negative. Rounds away from zero in halfway cases. + * + * @param numericExpr An expression that returns number when evaluated. + * @param decimalPlace The number of decimal places to round. + * @return A new {@link Expression} representing the round operation. + */ + @BetaApi + public static Expression roundToPrecision(Expression numericExpr, int decimalPlace) { + return new FunctionExpression("round", ImmutableList.of(numericExpr, constant(decimalPlace))); + } + + /** + * Creates an expression that rounds off {@code numericField} to {@code decimalPlace} decimal + * places if {@code decimalPlace} is positive, rounds off digits to the left of the decimal point + * if {@code decimalPlace} is negative. Rounds away from zero in halfway cases. + * + * @param numericField Name of field that returns number when evaluated. + * @param decimalPlace The number of decimal places to round. + * @return A new {@link Expression} representing the round operation. + */ + @BetaApi + public static Expression roundToPrecision(String numericField, int decimalPlace) { + return roundToPrecision(field(numericField), decimalPlace); + } + + /** + * Creates an expression that rounds off {@code numericExpr} to {@code decimalPlace} decimal + * places if {@code decimalPlace} is positive, rounds off digits to the left of the decimal point + * if {@code decimalPlace} is negative. Rounds away from zero in halfway cases. + * + * @param numericExpr An expression that returns number when evaluated. + * @param decimalPlace The number of decimal places to round. + * @return A new {@link Expression} representing the round operation. + */ + @BetaApi + public static Expression roundToPrecision(Expression numericExpr, Expression decimalPlace) { + return new FunctionExpression("round", ImmutableList.of(numericExpr, decimalPlace)); + } + + /** + * Creates an expression that rounds off {@code numericField} to {@code decimalPlace} decimal + * places if {@code decimalPlace} is positive, rounds off digits to the left of the decimal point + * if {@code decimalPlace} is negative. Rounds away from zero in halfway cases. + * + * @param numericField Name of field that returns number when evaluated. + * @param decimalPlace The number of decimal places to round. + * @return A new {@link Expression} representing the round operation. + */ + @BetaApi + public static Expression roundToPrecision(String numericField, Expression decimalPlace) { + return roundToPrecision(field(numericField), decimalPlace); + } + + /** + * Creates an expression that returns the smallest integer that isn't less than {@code + * numericExpr}. + * + * @param numericExpr An expression that returns number when evaluated. + * @return A new {@link Expression} representing an integer result from the ceil operation. + */ + @BetaApi + public static Expression ceil(Expression numericExpr) { + return new FunctionExpression("ceil", ImmutableList.of(numericExpr)); + } + + /** + * Creates an expression that returns the smallest integer that isn't less than {@code + * numericField}. + * + * @param numericField Name of field that returns number when evaluated. + * @return A new {@link Expression} representing an integer result from the ceil operation. + */ + @BetaApi + public static Expression ceil(String numericField) { + return ceil(field(numericField)); + } + + /** + * Creates an expression that returns the largest integer that isn't less than {@code + * numericExpr}. + * + * @param numericExpr An expression that returns number when evaluated. + * @return A new {@link Expression} representing an integer result from the floor operation. + */ + @BetaApi + public static Expression floor(Expression numericExpr) { + return new FunctionExpression("floor", ImmutableList.of(numericExpr)); + } + + /** + * Creates an expression that returns the largest integer that isn't less than {@code + * numericField}. + * + * @param numericField Name of field that returns number when evaluated. + * @return A new {@link Expression} representing an integer result from the floor operation. + */ + @BetaApi + public static Expression floor(String numericField) { + return floor(field(numericField)); + } + + /** + * Creates an expression that returns the {@code numericExpr} raised to the power of the {@code + * exponent}. Returns infinity on overflow and zero on underflow. + * + * @param numericExpr An expression that returns number when evaluated. + * @param exponent The numeric power to raise the {@code numericExpr}. + * @return A new {@link Expression} representing a numeric result from raising {@code numericExpr} + * to the power of {@code exponent}. + */ + @BetaApi + public static Expression pow(Expression numericExpr, Number exponent) { + return new FunctionExpression("pow", ImmutableList.of(numericExpr, constant(exponent))); + } + + /** + * Creates an expression that returns the {@code numericField} raised to the power of the {@code + * exponent}. Returns infinity on overflow and zero on underflow. + * + * @param numericField Name of field that returns number when evaluated. + * @param exponent The numeric power to raise the {@code numericField}. + * @return A new {@link Expression} representing a numeric result from raising {@code + * numericField} to the power of {@code exponent}. + */ + @BetaApi + public static Expression pow(String numericField, Number exponent) { + return pow(field(numericField), exponent); + } + + /** + * Creates an expression that returns the {@code numericExpr} raised to the power of the {@code + * exponent}. Returns infinity on overflow and zero on underflow. + * + * @param numericExpr An expression that returns number when evaluated. + * @param exponent The numeric power to raise the {@code numericExpr}. + * @return A new {@link Expression} representing a numeric result from raising {@code numericExpr} + * to the power of {@code exponent}. + */ + @BetaApi + public static Expression pow(Expression numericExpr, Expression exponent) { + return new FunctionExpression("pow", ImmutableList.of(numericExpr, exponent)); + } + + /** + * Creates an expression that returns the {@code numericField} raised to the power of the {@code + * exponent}. Returns infinity on overflow and zero on underflow. + * + * @param numericField Name of field that returns number when evaluated. + * @param exponent The numeric power to raise the {@code numericField}. + * @return A new {@link Expression} representing a numeric result from raising {@code + * numericField} to the power of {@code exponent}. + */ + @BetaApi + public static Expression pow(String numericField, Expression exponent) { + return pow(field(numericField), exponent); + } + + /** + * Creates an expression that returns the absolute value of {@code numericExpr}. + * + * @param numericExpr An expression that returns number when evaluated. + * @return A new {@link Expression} representing the numeric result of the absolute value + * operation. + */ + @BetaApi + public static Expression abs(Expression numericExpr) { + return new FunctionExpression("abs", ImmutableList.of(numericExpr)); + } + + /** + * Creates an expression that returns the absolute value of {@code numericField}. + * + * @param numericField Name of field that returns number when evaluated. + * @return A new {@link Expression} representing the numeric result of the absolute value + * operation. + */ + @BetaApi + public static Expression abs(String numericField) { + return abs(field(numericField)); + } + + /** + * Creates an expression that returns Euler's number e raised to the power of {@code numericExpr}. + * + * @param numericExpr An expression that returns number when evaluated. + * @return A new {@link Expression} representing the numeric result of the exponentiation. + */ + @BetaApi + public static Expression exp(Expression numericExpr) { + return new FunctionExpression("exp", ImmutableList.of(numericExpr)); + } + + /** + * Creates an expression that returns Euler's number e raised to the power of {@code + * numericField}. + * + * @param numericField Name of field that returns number when evaluated. + * @return A new {@link Expression} representing the numeric result of the exponentiation. + */ + @BetaApi + public static Expression exp(String numericField) { + return exp(field(numericField)); + } + + /** + * Creates an expression that returns the natural logarithm (base e) of {@code numericExpr}. + * + * @param numericExpr An expression that returns number when evaluated. + * @return A new {@link Expression} representing the numeric result of the natural logarithm. + */ + @BetaApi + public static Expression ln(Expression numericExpr) { + return new FunctionExpression("ln", ImmutableList.of(numericExpr)); + } + + /** + * Creates an expression that returns the natural logarithm (base e) of {@code numericField}. + * + * @param numericField Name of field that returns number when evaluated. + * @return A new {@link Expression} representing the numeric result of the natural logarithm. + */ + @BetaApi + public static Expression ln(String numericField) { + return ln(field(numericField)); + } + + /** + * Creates an expression that returns the logarithm of {@code numericExpr} with a given {@code + * base}. + * + * @param numericExpr An expression that returns number when evaluated. + * @param base The base of the logarithm. + * @return A new {@link Expression} representing a numeric result from the logarithm of {@code + * numericExpr} with a given {@code base}. + */ + @BetaApi + public static Expression log(Expression numericExpr, Number base) { + return new FunctionExpression("log", ImmutableList.of(numericExpr, constant(base))); + } + + /** + * Creates an expression that returns the logarithm of {@code numericField} with a given {@code + * base}. + * + * @param numericField Name of field that returns number when evaluated. + * @param base The base of the logarithm. + * @return A new {@link Expression} representing a numeric result from the logarithm of {@code + * numericField} with a given {@code base}. + */ + @BetaApi + public static Expression log(String numericField, Number base) { + return log(field(numericField), base); + } + + /** + * Creates an expression that returns the logarithm of {@code numericExpr} with a given {@code + * base}. + * + * @param numericExpr An expression that returns number when evaluated. + * @param base The base of the logarithm. + * @return A new {@link Expression} representing a numeric result from the logarithm of {@code + * numericExpr} with a given {@code base}. + */ + @BetaApi + public static Expression log(Expression numericExpr, Expression base) { + return new FunctionExpression("log", ImmutableList.of(numericExpr, base)); + } + + /** + * Creates an expression that returns the logarithm of {@code numericField} with a given {@code + * base}. + * + * @param numericField Name of field that returns number when evaluated. + * @param base The base of the logarithm. + * @return A new {@link Expression} representing a numeric result from the logarithm of {@code + * numericField} with a given {@code base}. + */ + @BetaApi + public static Expression log(String numericField, Expression base) { + return log(field(numericField), base); + } + + /** + * Creates an expression that returns the base 10 logarithm of {@code numericExpr}. + * + * @param numericExpr An expression that returns number when evaluated. + * @return A new {@link Expression} representing the numeric result of the base 10 logarithm. + */ + @BetaApi + public static Expression log10(Expression numericExpr) { + return new FunctionExpression("log10", ImmutableList.of(numericExpr)); + } + + /** + * Creates an expression that returns the base 10 logarithm of {@code numericField}. + * + * @param numericField Name of field that returns number when evaluated. + * @return A new {@link Expression} representing the numeric result of the base 10 logarithm. + */ + @BetaApi + public static Expression log10(String numericField) { + return log10(field(numericField)); + } + + /** + * Creates an expression that returns the square root of {@code numericExpr}. + * + * @param numericExpr An expression that returns number when evaluated. + * @return A new {@link Expression} representing the numeric result of the square root operation. + */ + @BetaApi + public static Expression sqrt(Expression numericExpr) { + return new FunctionExpression("sqrt", ImmutableList.of(numericExpr)); + } + + /** + * Creates an expression that returns the square root of {@code numericField}. + * + * @param numericField Name of field that returns number when evaluated. + * @return A new {@link Expression} representing the numeric result of the square root operation. + */ + @BetaApi + public static Expression sqrt(String numericField) { + return sqrt(field(numericField)); + } + + // Logical/Comparison Operations + /** + * Creates an expression that checks if the results of {@code expr} is NOT 'NaN' (Not a Number). + * + * @param expr The expression to check. + * @return A new {@link BooleanExpression} representing the isNotNan operation. + */ + @BetaApi + public static BooleanExpression isNotNaN(Expression expr) { + return new BooleanExpression("is_not_nan", expr); + } + + /** + * Creates an expression that checks if the results of this expression is NOT 'NaN' (Not a + * Number). + * + * @param fieldName The field to check. + * @return A new {@link BooleanExpression} representing the isNotNan operation. + */ + @BetaApi + public static BooleanExpression isNotNaN(String fieldName) { + return isNotNaN(field(fieldName)); + } + + /** + * Creates an expression that returns the largest value between multiple input expressions or + * literal values. Based on Firestore's value type ordering. + * + * @param expr The first operand expression. + * @param others Optional additional expressions or literals. + * @return A new {@link Expression} representing the logical maximum operation. + */ + @BetaApi + public static Expression logicalMaximum(Expression expr, Object... others) { + ImmutableList.Builder builder = ImmutableList.builder(); + builder.add(expr); + builder.addAll(toArrayOfExprOrConstant(others)); + return new FunctionExpression("maximum", builder.build()); + } + + /** + * Creates an expression that returns the largest value between multiple input expressions or + * literal values. Based on Firestore's value type ordering. + * + * @param fieldName The first operand field name. + * @param others Optional additional expressions or literals. + * @return A new {@link Expression} representing the logical maximum operation. + */ + @BetaApi + public static Expression logicalMaximum(String fieldName, Object... others) { + return logicalMaximum(field(fieldName), others); + } + + /** + * Creates an expression that returns the smallest value between multiple input expressions or + * literal values. Based on Firestore's value type ordering. + * + * @param expr The first operand expression. + * @param others Optional additional expressions or literals. + * @return A new {@link Expression} representing the logical minimum operation. + */ + @BetaApi + public static Expression logicalMinimum(Expression expr, Object... others) { + ImmutableList.Builder builder = ImmutableList.builder(); + builder.add(expr); + builder.addAll(toArrayOfExprOrConstant(others)); + return new FunctionExpression("minimum", builder.build()); + } + + /** + * Creates an expression that returns the smallest value between multiple input expressions or + * literal values. Based on Firestore's value type ordering. + * + * @param fieldName The first operand field name. + * @param others Optional additional expressions or literals. + * @return A new {@link Expression} representing the logical minimum operation. + */ + @BetaApi + public static Expression logicalMinimum(String fieldName, Object... others) { + return logicalMinimum(field(fieldName), others); + } + + /** + * Creates an expression that concatenates this expression with other values. + * + * @param others Optional additional expressions or constants to concatenate. + * @return A new {@link Expression} representing the concatenated value. + */ + @BetaApi + public Expression concat(Object... others) { + return Expression.concat(this, others); + } + + /** + * Creates an expression that returns a default value if this expression evaluates to an absent + * value. + * + * @param elseValue The default value. + * @return A new {@link Expression} representing the ifAbsent operation. + */ + @BetaApi + public Expression ifAbsent(Object elseValue) { + return Expression.ifAbsent(this, elseValue); + } + + /** + * Creates an expression that joins the elements of this array expression into a string. + * + * @param delimiter The delimiter to use. + * @return A new {@link Expression} representing the join operation. + */ + @BetaApi + public Expression join(String delimiter) { + return Expression.join(this, delimiter); + } + + /** + * Creates an expression that joins the elements of this array expression into a string. + * + * @param delimiter The delimiter to use. + * @return A new {@link Expression} representing the join operation. + */ + @BetaApi + public Expression join(Expression delimiter) { + return Expression.join(this, delimiter); + } + + /** + * Creates an expression that checks if the results of this expression is NOT 'NaN' (Not a + * Number). + * + * @return A new {@link BooleanExpression} representing the isNotNan operation. + */ + @BetaApi + public final BooleanExpression isNotNaN() { + return isNotNaN(this); + } + + /** + * Creates an expression that returns the largest value between multiple input expressions or + * literal values. Based on Firestore's value type ordering. + * + * @param others Optional additional expressions or literals. + * @return A new {@link Expression} representing the logical maximum operation. + */ + @BetaApi + public final Expression logicalMaximum(Object... others) { + return logicalMaximum(this, others); + } + + /** + * Creates an expression that returns the smallest value between multiple input expressions or + * literal values. Based on Firestore's value type ordering. + * + * @param others Optional additional expressions or literals. + * @return A new {@link Expression} representing the logical minimum operation. + */ + @BetaApi + public final Expression logicalMinimum(Object... others) { + return logicalMinimum(this, others); + } + + /** + * Creates an expression that rounds this numeric expression to nearest integer. + * + *

Rounds away from zero in halfway cases. + * + * @return A new {@link Expression} representing an integer result from the round operation. + */ + @BetaApi + public final Expression round() { + return round(this); + } + + /** + * Creates an expression that rounds off this numeric expression to {@code decimalPlace} decimal + * places if {@code decimalPlace} is positive, rounds off digits to the left of the decimal point + * if {@code decimalPlace} is negative. Rounds away from zero in halfway cases. + * + * @param decimalPlace The number of decimal places to round. + * @return A new {@link Expression} representing the round operation. + */ + @BetaApi + public final Expression roundToPrecision(int decimalPlace) { + return roundToPrecision(this, decimalPlace); + } + + /** + * Creates an expression that rounds off this numeric expression to {@code decimalPlace} decimal + * places if {@code decimalPlace} is positive, rounds off digits to the left of the decimal point + * if {@code decimalPlace} is negative. Rounds away from zero in halfway cases. + * + * @param decimalPlace The number of decimal places to round. + * @return A new {@link Expression} representing the round operation. + */ + @BetaApi + public final Expression roundToPrecision(Expression decimalPlace) { + return roundToPrecision(this, decimalPlace); + } + + /** + * Creates an expression that returns the smallest integer that isn't less than this numeric + * expression. + * + * @return A new {@link Expression} representing an integer result from the ceil operation. + */ + @BetaApi + public final Expression ceil() { + return ceil(this); + } + + /** + * Creates an expression that returns the largest integer that isn't less than this numeric + * expression. + * + * @return A new {@link Expression} representing an integer result from the floor operation. + */ + @BetaApi + public final Expression floor() { + return floor(this); + } + + /** + * Creates an expression that returns this numeric expression raised to the power of the {@code + * exponent}. Returns infinity on overflow and zero on underflow. + * + * @param exponent The numeric power to raise this numeric expression. + * @return A new {@link Expression} representing a numeric result from raising this numeric + * expression to the power of {@code exponent}. + */ + @BetaApi + public final Expression pow(Number exponent) { + return pow(this, exponent); + } + + /** + * Creates an expression that returns this numeric expression raised to the power of the {@code + * exponent}. Returns infinity on overflow and zero on underflow. + * + * @param exponent The numeric power to raise this numeric expression. + * @return A new {@link Expression} representing a numeric result from raising this numeric + * expression to the power of {@code exponent}. + */ + @BetaApi + public final Expression pow(Expression exponent) { + return pow(this, exponent); + } + + /** + * Creates an expression that returns the absolute value of this numeric expression. + * + * @return A new {@link Expression} representing the numeric result of the absolute value + * operation. + */ + @BetaApi + public final Expression abs() { + return abs(this); + } + + /** + * Creates an expression that returns Euler's number e raised to the power of this numeric + * expression. + * + * @return A new {@link Expression} representing the numeric result of the exponentiation. + */ + @BetaApi + public final Expression exp() { + return exp(this); + } + + /** + * Creates an expression that returns the natural logarithm (base e) of this numeric expression. + * + * @return A new {@link Expression} representing the numeric result of the natural logarithm. + */ + @BetaApi + public final Expression ln() { + return ln(this); + } + + /** + * Creates an expression that returns the base 10 logarithm of this numeric expression. + * + * @return A new {@link Expression} representing the numeric result of the base 10 logarithm. + */ + @BetaApi + public Expression log10() { + return Expression.log10(this); + } + + /** + * Creates an expression that returns the sum of the elements of this array expression. + * + * @return A new {@link Expression} representing the sum of the elements of the array. + */ + @BetaApi + public Expression arraySum() { + return Expression.arraySum(this); + } + + /** + * Creates an expression that returns the square root of this numeric expression. + * + * @return A new {@link Expression} representing the numeric result of the square root operation. + */ + @BetaApi + public final Expression sqrt() { + return sqrt(this); + } + + // Fluent API + /** + * Creates an expression that adds this numeric expression to another numeric expression. + * + * @param other Numeric expression to add. + * @return A new {@link Expression} representing the addition operation. + */ + @BetaApi + public final Expression add(Object other) { + return add(this, toExprOrConstant(other)); + } + + /** + * Creates an expression that subtracts a numeric expressions from this numeric expression. + * + * @param other Constant to subtract. + * @return A new {@link Expression} representing the subtract operation. + */ + @BetaApi + public final Expression subtract(Object other) { + return subtract(this, toExprOrConstant(other)); + } + + /** + * Creates an expression that multiplies this numeric expression with another numeric expression. + * + * @param other Numeric expression to multiply. + * @return A new {@link Expression} representing the multiplication operation. + */ + @BetaApi + public final Expression multiply(Object other) { + return multiply(this, toExprOrConstant(other)); + } + + /** + * Creates an expression that divides this numeric expression by another numeric expression. + * + * @param other Numeric expression to divide this numeric expression by. + * @return A new {@link Expression} representing the division operation. + */ + @BetaApi + public final Expression divide(Object other) { + return divide(this, toExprOrConstant(other)); + } + + /** + * Creates an expression that calculates the modulo (remainder) of dividing this numeric + * expressions by another numeric expression. + * + * @param other The numeric expression to divide this expression by. + * @return A new {@link Expression} representing the modulo operation. + */ + @BetaApi + public final Expression mod(Object other) { + return mod(this, toExprOrConstant(other)); + } + + /** + * Creates an expression that checks if this expression is equal to a {@code value}. + * + * @param other The value to compare to. + * @return A new {@link BooleanExpression} representing the equality comparison. + */ + @BetaApi + public final BooleanExpression equal(Object other) { + return equal(this, toExprOrConstant(other)); + } + + /** + * Creates an expression that checks if this expression is not equal to a {@code value}. + * + * @param other The value to compare to. + * @return A new {@link BooleanExpression} representing the inequality comparison. + */ + @BetaApi + public final BooleanExpression notEqual(Object other) { + return notEqual(this, toExprOrConstant(other)); + } + + /** + * Creates an expression that checks if this expression is greater than a {@code value}. + * + * @param other The value to compare to. + * @return A new {@link BooleanExpression} representing the greater than comparison. + */ + @BetaApi + public final BooleanExpression greaterThan(Object other) { + return greaterThan(this, toExprOrConstant(other)); + } + + /** + * Creates an expression that checks if this expression is greater than or equal to a {@code + * value}. + * + * @param other The value to compare to. + * @return A new {@link BooleanExpression} representing the greater than or equal to comparison. + */ + @BetaApi + public final BooleanExpression greaterThanOrEqual(Object other) { + return greaterThanOrEqual(this, toExprOrConstant(other)); + } + + /** + * Creates an expression that checks if this expression is less than a value. + * + * @param other The value to compare to. + * @return A new {@link BooleanExpression} representing the less than comparison. + */ + @BetaApi + public final BooleanExpression lessThan(Object other) { + return lessThan(this, toExprOrConstant(other)); + } + + /** + * Creates an expression that checks if this expression is less than or equal to a {@code value}. + * + * @param other The value to compare to. + * @return A new {@link BooleanExpression} representing the less than or equal to comparison. + */ + @BetaApi + public final BooleanExpression lessThanOrEqual(Object other) { + return lessThanOrEqual(this, toExprOrConstant(other)); + } + + /** + * Creates an expression that checks if this expression, when evaluated, is equal to any of the + * provided {@code values}. + * + * @param other The values to check against. + * @return A new {@link BooleanExpression} representing the 'IN' comparison. + */ + @BetaApi + public final BooleanExpression equalAny(List other) { + return equalAny(this, other); + } + + /** + * Creates an expression that checks if this expression, when evaluated, is not equal to all the + * provided {@code values}. + * + * @param other The values to check against. + * @return A new {@link BooleanExpression} representing the 'NOT IN' comparison. + */ + @BetaApi + public final BooleanExpression notEqualAny(List other) { + return notEqualAny(this, other); + } + + /** + * Creates an expression that calculates the character length of this string expression in UTF8. + * + * @return A new {@link Expression} representing the charLength operation. + */ + @BetaApi + public final Expression charLength() { + return charLength(this); + } + + /** + * Creates an expression that calculates the length of a string in UTF-8 bytes, or just the length + * of a Blob. + * + * @return A new {@link Expression} representing the length of the string in bytes. + */ + @BetaApi + public final Expression byteLength() { + return byteLength(this); + } + + /** + * Creates an expression that calculates the length of the expression if it is a string, array, + * map, or Blob. + * + * @return A new {@link Expression} representing the length of the expression. + */ + @BetaApi + public final Expression length() { + return length(this); + } + + /** + * Creates an expression that performs a case-sensitive wildcard string comparison. + * + * @param pattern The pattern to search for. You can use "%" as a wildcard character. + * @return A new {@link BooleanExpression} representing the like operation. + */ + @BetaApi + public final BooleanExpression like(Object pattern) { + return like(this, toExprOrConstant(pattern)); + } + + /** + * Creates an expression that checks if this string expression contains a specified regular + * expression as a substring. + * + * @param pattern The regular expression to use for the search. + * @return A new {@link BooleanExpression} representing the contains regular expression + * comparison. + */ + @BetaApi + public final BooleanExpression regexContains(Object pattern) { + return regexContains(this, toExprOrConstant(pattern)); + } + + /** + * Creates an expression that checks if this string expression matches a specified regular + * expression. + * + * @param pattern The regular expression to use for the match. + * @return A new {@link BooleanExpression} representing the regular expression match comparison. + */ + @BetaApi + public final BooleanExpression regexMatch(Object pattern) { + return regexMatch(this, toExprOrConstant(pattern)); + } + + /** + * Creates an expression that checks if this string expression contains a specified substring. + * + * @param substring The expression representing the substring to search for. + * @return A new {@link BooleanExpression} representing the contains comparison. + */ + @BetaApi + public final BooleanExpression stringContains(Object substring) { + return stringContains(this, toExprOrConstant(substring)); + } + + /** + * Creates an expression that checks if this string expression starts with a given {@code prefix}. + * + * @param prefix The prefix string expression to check for. + * @return A new {@link Expression} representing the the 'starts with' comparison. + */ + @BetaApi + public final BooleanExpression startsWith(Object prefix) { + return startsWith(this, toExprOrConstant(prefix)); + } + + /** + * Creates an expression that checks if this string expression ends with a given {@code suffix}. + * + * @param suffix The suffix string expression to check for. + * @return A new {@link Expression} representing the 'ends with' comparison. + */ + @BetaApi + public final BooleanExpression endsWith(Object suffix) { + return endsWith(this, toExprOrConstant(suffix)); + } + + /** + * Creates an expression that returns a substring of the given string. + * + * @param index The starting index of the substring. + * @param length The length of the substring. + * @return A new {@link Expression} representing the substring. + */ + @BetaApi + public final Expression substring(Object index, Object length) { + return substring(this, toExprOrConstant(index), toExprOrConstant(length)); + } + + /** + * Creates an expression that converts this string expression to lowercase. + * + * @return A new {@link Expression} representing the lowercase string. + */ + @BetaApi + public final Expression toLower() { + return toLower(this); + } + + /** + * Creates an expression that converts this string expression to uppercase. + * + * @return A new {@link Expression} representing the lowercase string. + */ + @BetaApi + public final Expression toUpper() { + return toUpper(this); + } + + /** + * Creates an expression that removes leading and trailing whitespace from this string expression. + * + * @return A new {@link Expression} representing the trimmed string. + */ + @BetaApi + public final Expression trim() { + return trim(this); + } + + /** + * Creates an expression that removes specified characters from the beginning and end of this + * string or blob expression. + * + * @param characters The characters to remove. + * @return A new {@link Expression} representing the trimmed string or blob. + */ + @BetaApi + public Expression trimValue(String characters) { + return trimValue(this, characters); + } + + /** + * Creates an expression that removes specified characters from the beginning and end of this + * string or blob expression. + * + * @param characters The expression representing the characters to remove. + * @return A new {@link Expression} representing the trimmed string or blob. + */ + @BetaApi + public Expression trimValue(Expression characters) { + return trimValue(this, characters); + } + + /** + * Creates an expression that splits this string or blob expression by a delimiter. + * + * @param delimiter The delimiter to split by. + * @return A new {@link Expression} representing the split string or blob as an array. + */ + @BetaApi + public Expression split(Expression delimiter) { + return split(this, delimiter); + } + + /** + * Creates an expression that splits this string or blob expression by a delimiter. + * + * @param delimiter The delimiter to split by. + * @return A new {@link Expression} representing the split string or blob as an array. + */ + @BetaApi + public Expression split(String delimiter) { + return split(this, delimiter); + } + + /** + * Creates an expression that concatenates string expressions and string constants together. + * + * @param others The string expressions or string constants to concatenate. + * @return A new {@link Expression} representing the concatenated string. + */ + @BetaApi + public final Expression stringConcat(String... others) { + return stringConcat(this, others); + } + + /** + * Creates an expression that concatenates string expressions together. + * + * @param others The string expressions or string constants to concatenate. + * @return A new {@link Expression} representing the concatenated string. + */ + @BetaApi + public final Expression stringConcat(Expression... others) { + return stringConcat(this, others); + } + + /** + * Accesses a map (object) value using the provided {@code key}. + * + * @param key The key to access in the map. + * @return A new {@link Expression} representing the value associated with the given key in the + * map. + */ + @BetaApi + public final Expression mapGet(Object key) { + return mapGet(this, toExprOrConstant(key)); + } + + /** + * Creates an expression that returns true if yhe result of this expression is absent. Otherwise, + * returns false even if the value is null. + * + * @return A new {@link BooleanExpression} representing the isAbsent operation. + */ + @BetaApi + public final BooleanExpression isAbsent() { + return isAbsent(this); + } + + /** + * Creates an expression that checks if this expression evaluates to 'NaN' (Not a Number). + * + * @return A new {@link BooleanExpression} representing the isNan operation. + */ + @BetaApi + public final BooleanExpression isNaN() { + return isNaN(this); + } + + /** + * Creates an expression that checks if tbe result of this expression is null. + * + * @return A new {@link BooleanExpression} representing the isNull operation. + */ + @BetaApi + public final BooleanExpression isNull() { + return isNull(this); + } + + /** + * Creates an expression that checks if tbe result of this expression is not null. + * + * @return A new {@link BooleanExpression} representing the isNotNull operation. + */ + @BetaApi + public final BooleanExpression isNotNull() { + return isNotNull(this); + } + + /** + * Creates an aggregation that calculates the sum of this numeric expression across multiple stage + * inputs. + * + * @return A new {@link AggregateFunction} representing the sum aggregation. + */ + @BetaApi + public final AggregateFunction sum() { + return AggregateFunction.sum(this); + } + + /** + * Creates an aggregation that calculates the average (mean) of this numeric expression across + * multiple stage inputs. + * + * @return A new {@link AggregateFunction} representing the average aggregation. + */ + @BetaApi + public final AggregateFunction average() { + return AggregateFunction.average(this); + } + + /** + * Creates an aggregation that finds the minimum value of this expression across multiple stage + * inputs. + * + * @return A new {@link AggregateFunction} representing the minimum aggregation. + */ + @BetaApi + public final AggregateFunction minimum() { + return AggregateFunction.minimum(this); + } + + /** + * Creates an aggregation that finds the maximum value of this expression across multiple stage + * inputs. + * + * @return A new {@link AggregateFunction} representing the maximum aggregation. + */ + @BetaApi + public final AggregateFunction maximum() { + return AggregateFunction.maximum(this); + } + + /** + * Creates an aggregation that counts the number of stage inputs with valid evaluations of the + * this expression. + * + * @return A new {@link AggregateFunction} representing the count aggregation. + */ + @BetaApi + public final AggregateFunction count() { + return AggregateFunction.count(this); + } + + /** + * Creates an aggregation that counts the number of distinct values of this expression. + * + * @return A new {@link AggregateFunction} representing the count distinct aggregation. + */ + @BetaApi + public final AggregateFunction countDistinct() { + return AggregateFunction.countDistinct(this); + } + + /** + * Create an {@link Ordering} that sorts documents in ascending order based on value of this + * expression + * + * @return A new {@link Ordering} object with ascending sort by this expression. + */ + @BetaApi + public final Ordering ascending() { + return Ordering.ascending(this); + } + + /** + * Create an {@link Ordering} that sorts documents in descending order based on value of this + * expression + * + * @return A new {@link Ordering} object with descending sort by this expression. + */ + @BetaApi + public final Ordering descending() { + return Ordering.descending(this); + } + + /** + * Assigns an alias to this expression. + * + *

Aliases are useful for renaming fields in the output of a stage or for giving meaningful + * names to calculated values. + * + * @param alias The alias to assign to this expression. + * @return A new {@link Selectable} (typically an {@link AliasedExpression}) that wraps this + * expression and associates it with the provided alias. + */ + @BetaApi + public Selectable as(String alias) { + return new AliasedExpression(this, alias); + } + + // Fluent API for new functions + /** + * Creates an expression that merges multiple maps into a single map. If multiple maps have the + * same key, the later value is used. + * + * @param secondMap Map expression that will be merged. + * @param otherMaps Additional maps to merge. + * @return A new {@link Expression} representing the mapMerge operation. + */ + @BetaApi + public final Expression mapMerge(Expression secondMap, Expression... otherMaps) { + return mapMerge(this, secondMap, otherMaps); + } + + /** + * Creates an expression that removes a key from this map expression. + * + * @param key The name of the key to remove from this map expression. + * @return A new {@link Expression} that evaluates to a modified map. + */ + @BetaApi + public final Expression mapRemove(Expression key) { + return mapRemove(this, key); + } + + /** + * Creates an expression that removes a key from this map expression. + * + * @param key The name of the key to remove from this map expression. + * @return A new {@link Expression} that evaluates to a modified map. + */ + @BetaApi + public final Expression mapRemove(String key) { + return mapRemove(this, key); + } + + /** + * Creates an expression that reverses this expression, which must be a string, blob, or array. + * + * @return A new {@link Expression} representing the reversed value. + */ + @BetaApi + public final Expression reverse() { + return reverse(this); + } + + /** + * Creates an expression that concatenates a field's array value with other arrays. + * + * @param otherArrays Optional additional array expressions or array literals to concatenate. + * @return A new {@link Expression} representing the arrayConcat operation. + */ + @BetaApi + public final Expression arrayConcat(Expression... otherArrays) { + return arrayConcat(this, otherArrays); + } + + /** + * Reverses the order of elements in the array. + * + * @return A new {@link Expression} representing the arrayReverse operation. + */ + @BetaApi + public final Expression arrayReverse() { + return arrayReverse(this); + } + + /** + * Creates an expression that checks if array contains a specific {@code element}. + * + * @param element The element to search for in the array. + * @return A new {@link BooleanExpression} representing the arrayContains operation. + */ + @BetaApi + public final BooleanExpression arrayContains(Object element) { + return arrayContains(this, element); + } + + /** + * Creates an expression that checks if array contains all the specified {@code values}. + * + * @param values The elements to check for in the array. + * @return A new {@link BooleanExpression} representing the arrayContainsAll operation. + */ + @BetaApi + public final BooleanExpression arrayContainsAll(List values) { + return arrayContainsAll(this, values); + } + + /** + * Creates an expression that checks if array contains all elements of {@code arrayExpression}. + * + * @param arrayExpression The elements to check for in the array. + * @return A new {@link BooleanExpression} representing the arrayContainsAll operation. + */ + @BetaApi + public final BooleanExpression arrayContainsAll(Expression arrayExpression) { + return arrayContainsAll(this, arrayExpression); + } + + /** + * Creates an expression that checks if array contains any of the specified {@code values}. + * + * @param values The elements to check for in the array. + * @return A new {@link BooleanExpression} representing the arrayContainsAny operation. + */ + @BetaApi + public final BooleanExpression arrayContainsAny(List values) { + return arrayContainsAny(this, values); + } + + /** + * Creates an expression that checks if array contains any elements of {@code arrayExpression}. + * + * @param arrayExpression The elements to check for in the array. + * @return A new {@link BooleanExpression} representing the arrayContainsAny operation. + */ + @BetaApi + public final BooleanExpression arrayContainsAny(Expression arrayExpression) { + return arrayContainsAny(this, arrayExpression); + } + + /** + * Creates an expression that calculates the length of an array expression. + * + * @return A new {@link Expression} representing the length of the array. + */ + @BetaApi + public final Expression arrayLength() { + return arrayLength(this); + } + + /** + * Creates an expression that indexes into an array from the beginning or end and return the + * element. If the offset exceeds the array length, an error is returned. A negative offset, + * starts from the end. + * + * @param offset An Expression evaluating to the index of the element to return. + * @return A new {@link Expression} representing the arrayGet operation. + */ + @BetaApi + public final Expression arrayGet(Expression offset) { + return arrayGet(this, offset); + } + + /** + * Creates an expression that indexes into an array from the beginning or end and return the + * element. If the offset exceeds the array length, an error is returned. A negative offset, + * starts from the end. + * + * @param offset An Expression evaluating to the index of the element to return. + * @return A new {@link Expression} representing the arrayOffset operation. + */ + @BetaApi + public final Expression arrayGet(int offset) { + return arrayGet(this, offset); + } + + /** + * Calculates the Cosine distance between this and another vector expressions. + * + * @param vector The other vector (represented as an Expression) to compare against. + * @return A new {@link Expression} representing the cosine distance between the two vectors. + */ + @BetaApi + public final Expression cosineDistance(Expression vector) { + return cosineDistance(this, vector); + } + + /** + * Calculates the Cosine distance between this vector expression and a vector literal. + * + * @param vector The other vector (as an array of doubles) to compare against. + * @return A new {@link Expression} representing the cosine distance between the two vectors. + */ + @BetaApi + public final Expression cosineDistance(double[] vector) { + return cosineDistance(this, vector); + } + + /** + * Calculates the dot product distance between this and another vector expression. + * + * @param vector The other vector (represented as an Expression) to compare against. + * @return A new {@link Expression} representing the dot product distance between the two vectors. + */ + @BetaApi + public final Expression dotProduct(Expression vector) { + return dotProduct(this, vector); + } + + /** + * Calculates the dot product distance between this vector expression and a vector literal. + * + * @param vector The other vector (as an array of doubles) to compare against. + * @return A new {@link Expression} representing the dot product distance between the two vectors. + */ + @BetaApi + public final Expression dotProduct(double[] vector) { + return dotProduct(this, vector); + } + + /** + * Calculates the Euclidean distance between this and another vector expression. + * + * @param vector The other vector (represented as an Expression) to compare against. + * @return A new {@link Expression} representing the Euclidean distance between the two vectors. + */ + @BetaApi + public final Expression euclideanDistance(Expression vector) { + return euclideanDistance(this, vector); + } + + /** + * Calculates the Euclidean distance between this vector expression and a vector literal. + * + * @param vector The other vector (as an array of doubles) to compare against. + * @return A new {@link Expression} representing the Euclidean distance between the two vectors. + */ + @BetaApi + public final Expression euclideanDistance(double[] vector) { + return euclideanDistance(this, vector); + } + + /** + * Creates an expression that calculates the length (dimension) of a Firestore Vector. + * + * @return A new {@link Expression} representing the length (dimension) of the vector. + */ + @BetaApi + public final Expression vectorLength() { + return vectorLength(this); + } + + /** + * Creates an expression that interprets this expression as the number of microseconds since the + * Unix epoch (1970-01-01 00:00:00 UTC) and returns a timestamp. + * + * @return A new {@link Expression} representing the timestamp. + */ + @BetaApi + public final Expression unixMicrosToTimestamp() { + return unixMicrosToTimestamp(this); + } + + /** + * Creates an expression that converts this timestamp expression to the number of microseconds + * since the Unix epoch (1970-01-01 00:00:00 UTC). + * + * @return A new {@link Expression} representing the number of microseconds since epoch. + */ + @BetaApi + public final Expression timestampToUnixMicros() { + return timestampToUnixMicros(this); + } + + /** + * Creates an expression that interprets this expression as the number of milliseconds since the + * Unix epoch (1970-01-01 00:00:00 UTC) and returns a timestamp. + * + * @return A new {@link Expression} representing the timestamp. + */ + @BetaApi + public final Expression unixMillisToTimestamp() { + return unixMillisToTimestamp(this); + } + + /** + * Creates an expression that converts this timestamp expression to the number of milliseconds + * since the Unix epoch (1970-01-01 00:00:00 UTC). + * + * @return A new {@link Expression} representing the number of milliseconds since epoch. + */ + @BetaApi + public final Expression timestampToUnixMillis() { + return timestampToUnixMillis(this); + } + + /** + * Creates an expression that interprets this expression as the number of seconds since the Unix + * epoch (1970-01-01 00:00:00 UTC) and returns a timestamp. + * + * @return A new {@link Expression} representing the timestamp. + */ + @BetaApi + public final Expression unixSecondsToTimestamp() { + return unixSecondsToTimestamp(this); + } + + /** + * Creates an expression that converts this timestamp expression to the number of seconds since + * the Unix epoch (1970-01-01 00:00:00 UTC). + * + * @return A new {@link Expression} representing the number of seconds since epoch. + */ + @BetaApi + public final Expression timestampToUnixSeconds() { + return timestampToUnixSeconds(this); + } + + /** + * Creates an expression that adds a specified amount of time to this timestamp expression. + * + * @param unit The expression representing the unit of time to add. Valid units include + * "microsecond", "millisecond", "second", "minute", "hour" and "day". + * @param amount The expression representing the amount of time to add. + * @return A new {@link Expression} representing the resulting timestamp. + */ + @BetaApi + public final Expression timestampAdd(Expression unit, Expression amount) { + return timestampAdd(this, unit, amount); + } + + /** + * Creates an expression that adds a specified amount of time to this timestamp expression. + * + * @param unit The unit of time to add. Valid units include "microsecond", "millisecond", + * "second", "minute", "hour" and "day". + * @param amount The amount of time to add. + * @return A new {@link Expression} representing the resulting timestamp. + */ + @BetaApi + public final Expression timestampAdd(String unit, long amount) { + return timestampAdd(this, unit, amount); + } + + /** + * Creates an expression that subtracts a specified amount of time to this timestamp expression. + * + * @param unit The expression representing the unit of time to subtract. Valid units include + * "microsecond", "millisecond", "second", "minute", "hour" and "day". + * @param amount The expression representing the amount of time to subtract. + * @return A new {@link Expression} representing the resulting timestamp. + */ + @BetaApi + public final Expression timestampSubtract(Expression unit, Expression amount) { + return timestampSubtract(this, unit, amount); + } + + /** + * Creates an expression that subtracts a specified amount of time to this timestamp expression. + * + * @param unit The unit of time to subtract. Valid units include "microsecond", "millisecond", + * "second", "minute", "hour" and "day". + * @param amount The amount of time to subtract. + * @return A new {@link Expression} representing the resulting timestamp. + */ + @BetaApi + public final Expression timestampSubtract(String unit, long amount) { + return timestampSubtract(this, unit, amount); + } + + /** + * Creates an expression that truncates this timestamp expression to a specified granularity. + * + * @param granularity The granularity to truncate to. Valid values are "microsecond", + * "millisecond", "second", "minute", "hour", "day", "week", "week(monday)", "week(tuesday)", + * "week(wednesday)", "week(thursday)", "week(friday)", "week(saturday)", "week(sunday)", + * "isoweek", "month", "quarter", "year", and "isoyear". + * @return A new {@link Expression} representing the truncated timestamp. + */ + @BetaApi + public final Expression timestampTruncate(String granularity) { + return timestampTruncate(this, granularity); + } + + /** + * Creates an expression that truncates this timestamp expression to a specified granularity. + * + * @param granularity The granularity expression to truncate to. Valid values are "microsecond", + * "millisecond", "second", "minute", "hour", "day", "week", "week(monday)", "week(tuesday)", + * "week(wednesday)", "week(thursday)", "week(friday)", "week(saturday)", "week(sunday)", + * "isoweek", "month", "quarter", "year", and "isoyear". + * @return A new {@link Expression} representing the truncated timestamp. + */ + @BetaApi + public final Expression timestampTruncate(Expression granularity) { + return timestampTruncate(this, granularity); + } + + /** + * Creates an expression that checks if this expression evaluates to a name of the field that + * exists. + * + * @return A new {@link Expression} representing the exists check. + */ + @BetaApi + public final BooleanExpression exists() { + return exists(this); + } + + /** + * Creates an expression that returns the {@code catchExpr} argument if there is an error, else + * return the result of this expression. + * + * @param catchExpr The catch expression that will be evaluated and returned if the this + * expression produces an error. + * @return A new {@link Expression} representing the ifError operation. + */ + @BetaApi + public final Expression ifError(Expression catchExpr) { + return ifError(this, catchExpr); + } + + /** + * Creates an expression that returns the {@code catchValue} argument if there is an error, else + * return the result of this expression. + * + * @param catchValue The value that will be returned if this expression produces an error. + * @return A new {@link Expression} representing the ifError operation. + */ + @BetaApi + public final Expression ifError(Object catchValue) { + return ifError(this, catchValue); + } + + /** + * Creates an expression that checks if this expression produces an error. + * + * @return A new {@link BooleanExpression} representing the `isError` check. + */ + @BetaApi + public final BooleanExpression isError() { + return isError(this); + } + + /** + * Creates an expression that returns the document ID from this path expression. + * + * @return A new {@link Expression} representing the documentId operation. + */ + @BetaApi + public final Expression documentId() { + return documentId(this); + } + + /** + * Creates an expression that returns the collection ID from this path expression. + * + * @return A new {@link Expression} representing the collectionId operation. + */ + @BetaApi + public final Expression collectionId() { + return collectionId(this); + } + + /** + * Creates an expression that returns a string indicating the type of the value this expression + * evaluates to. + * + * @return A new {@link Expression} representing the type operation. + */ + @BetaApi + public final Expression type() { + return type(this); + } +} diff --git a/google-cloud-firestore/src/main/java/com/google/cloud/firestore/pipeline/expressions/Field.java b/google-cloud-firestore/src/main/java/com/google/cloud/firestore/pipeline/expressions/Field.java new file mode 100644 index 000000000..0b5729040 --- /dev/null +++ b/google-cloud-firestore/src/main/java/com/google/cloud/firestore/pipeline/expressions/Field.java @@ -0,0 +1,114 @@ +/* + * Copyright 2024 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.pipeline.expressions; + +import com.google.api.core.BetaApi; +import com.google.api.core.InternalApi; +import com.google.cloud.firestore.FieldPath; +import com.google.cloud.firestore.Pipeline; +import com.google.common.base.Objects; +import com.google.firestore.v1.Value; +import javax.annotation.Nullable; + +/** + * Represents a reference to a field in a Firestore document. + * + *

Field references are used to access document field values in expressions and to specify fields + * for sorting, filtering, and projecting data in Firestore pipelines. + * + *

You can create a `Field` instance using the static {@link #field(String)} method: + * + *

{@code
+ * // Create a Field instance for the 'name' field
+ * Field nameField = Field.of("name");
+ *
+ * // Create a Field instance for a nested field 'address.city'
+ * Field cityField = Field.of("address.city");
+ * }
+ */ +@BetaApi +public final class Field extends Expression implements Selectable { + public static final String DOCUMENT_ID = "__name__"; + private final FieldPath path; + @Nullable private Pipeline pipeline; // Nullable + + private Field(FieldPath path) { + this.path = path; + } + + /** + * Creates a {@code Field} instance representing the field at the given path. + * + *

The path can be a simple field name (e.g., "name") or a dot-separated path to a nested field + * (e.g., "address.city"). + * + *

Example: + * + *

{@code
+   * // Create a Field instance for the 'title' field
+   * Field titleField = Field.of("title");
+   *
+   * // Create a Field instance for a nested field 'author.firstName'
+   * Field authorFirstNameField = Field.of("author.firstName");
+   * }
+ * + * @param path The path to the field. + * @return A new {@code Field} instance representing the specified field. + */ + @InternalApi + public static Field ofUserPath(String path) { + if (path.equals(DOCUMENT_ID)) { + return new Field(FieldPath.documentId()); + } + return new Field(FieldPath.fromDotSeparatedString(path)); + } + + @InternalApi + public static Field ofServerPath(String path) { + if (path.equals(DOCUMENT_ID)) { + return new Field(FieldPath.documentId()); + } + return new Field(FieldPath.fromServerFormat(path)); + } + + @InternalApi + public Value toProto() { + return Value.newBuilder().setFieldReferenceValue(path.toString()).build(); + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + Field field = (Field) o; + return Objects.equal(path, field.path) && Objects.equal(pipeline, field.pipeline); + } + + @Override + public int hashCode() { + return Objects.hashCode(path, pipeline); + } + + @InternalApi + public FieldPath getPath() { + return path; + } +} diff --git a/google-cloud-firestore/src/main/java/com/google/cloud/firestore/pipeline/expressions/FunctionExpression.java b/google-cloud-firestore/src/main/java/com/google/cloud/firestore/pipeline/expressions/FunctionExpression.java new file mode 100644 index 000000000..eacbf953e --- /dev/null +++ b/google-cloud-firestore/src/main/java/com/google/cloud/firestore/pipeline/expressions/FunctionExpression.java @@ -0,0 +1,63 @@ +/* + * Copyright 2024 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.pipeline.expressions; + +import com.google.api.core.BetaApi; +import com.google.api.core.InternalApi; +import com.google.common.base.Objects; +import com.google.firestore.v1.Value; +import java.util.Collections; +import java.util.List; +import java.util.stream.Collectors; + +@BetaApi +public class FunctionExpression extends Expression { + private final String name; + private final List params; + + FunctionExpression(String name, List params) { + this.name = name; + this.params = Collections.unmodifiableList(params); + } + + @InternalApi + @Override + Value toProto() { + return Value.newBuilder() + .setFunctionValue( + com.google.firestore.v1.Function.newBuilder() + .setName(this.name) + .addAllArgs( + this.params.stream() + .map(FunctionUtils::exprToValue) + .collect(Collectors.toList()))) + .build(); + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + FunctionExpression that = (FunctionExpression) o; + return Objects.equal(name, that.name) && Objects.equal(params, that.params); + } + + @Override + public int hashCode() { + return Objects.hashCode(name, params); + } +} diff --git a/google-cloud-firestore/src/main/java/com/google/cloud/firestore/pipeline/expressions/FunctionUtils.java b/google-cloud-firestore/src/main/java/com/google/cloud/firestore/pipeline/expressions/FunctionUtils.java new file mode 100644 index 000000000..85c997902 --- /dev/null +++ b/google-cloud-firestore/src/main/java/com/google/cloud/firestore/pipeline/expressions/FunctionUtils.java @@ -0,0 +1,33 @@ +/* + * Copyright 2024 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.pipeline.expressions; + +import com.google.api.core.InternalApi; +import com.google.firestore.v1.Value; + +@InternalApi +public final class FunctionUtils { + @InternalApi + public static Value exprToValue(Expression expr) { + return (expr == null ? Constant.NULL : expr).toProto(); + } + + @InternalApi + public static Value aggregateFunctionToValue(AggregateFunction expr) { + return expr.toProto(); + } +} diff --git a/google-cloud-firestore/src/main/java/com/google/cloud/firestore/pipeline/expressions/Ordering.java b/google-cloud-firestore/src/main/java/com/google/cloud/firestore/pipeline/expressions/Ordering.java new file mode 100644 index 000000000..e6f27dfd5 --- /dev/null +++ b/google-cloud-firestore/src/main/java/com/google/cloud/firestore/pipeline/expressions/Ordering.java @@ -0,0 +1,79 @@ +/* + * Copyright 2024 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.pipeline.expressions; + +import static com.google.cloud.firestore.PipelineUtils.encodeValue; + +import com.google.api.core.BetaApi; +import com.google.api.core.InternalApi; +import com.google.firestore.v1.MapValue; +import com.google.firestore.v1.Value; +import java.util.Locale; + +@BetaApi +public final class Ordering { + + private final Expression expr; + private final Ordering.Direction dir; + + private Ordering(Expression expr, Ordering.Direction dir) { + this.expr = expr; + this.dir = dir; + } + + @BetaApi + public enum Direction { + ASCENDING, + DESCENDING; + + @Override + public String toString() { + return name().toLowerCase(Locale.getDefault()); + } + } + + @InternalApi + public Value toProto() { + return Value.newBuilder() + .setMapValue( + MapValue.newBuilder() + .putFields("direction", encodeValue(dir.toString())) + .putFields("expression", expr.toProto()) + .build()) + .build(); + } + + @BetaApi + public static Ordering ascending(Expression expr) { + return new Ordering(expr, Direction.ASCENDING); + } + + @BetaApi + public static Ordering descending(Expression expr) { + return new Ordering(expr, Direction.DESCENDING); + } + + @InternalApi + public Expression getExpr() { + return expr; + } + + @InternalApi + public Ordering.Direction getDir() { + return dir; + } +} diff --git a/google-cloud-firestore/src/main/java/com/google/cloud/firestore/pipeline/expressions/Selectable.java b/google-cloud-firestore/src/main/java/com/google/cloud/firestore/pipeline/expressions/Selectable.java new file mode 100644 index 000000000..cf7bc71c0 --- /dev/null +++ b/google-cloud-firestore/src/main/java/com/google/cloud/firestore/pipeline/expressions/Selectable.java @@ -0,0 +1,22 @@ +/* + * Copyright 2024 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.pipeline.expressions; + +import com.google.api.core.BetaApi; + +@BetaApi +public interface Selectable {} diff --git a/google-cloud-firestore/src/main/java/com/google/cloud/firestore/pipeline/stages/AbstractOptions.java b/google-cloud-firestore/src/main/java/com/google/cloud/firestore/pipeline/stages/AbstractOptions.java new file mode 100644 index 000000000..ca1491cd6 --- /dev/null +++ b/google-cloud-firestore/src/main/java/com/google/cloud/firestore/pipeline/stages/AbstractOptions.java @@ -0,0 +1,99 @@ +/* + * Copyright 2024 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.pipeline.stages; + +import static com.google.cloud.firestore.PipelineUtils.encodeValue; + +import com.google.cloud.firestore.PipelineUtils; +import com.google.cloud.firestore.pipeline.expressions.Expression; +import com.google.cloud.firestore.pipeline.expressions.Field; +import com.google.cloud.firestore.pipeline.expressions.FunctionUtils; +import com.google.common.collect.ImmutableMap; +import com.google.common.collect.Lists; +import com.google.firestore.v1.Value; +import java.util.Arrays; +import java.util.List; + +/** + * Parent class to Pipeline and Stage options. + * + *

Provides a base set of `wither` methods for adding undefined options. + * + *

Standardizes structure of options for uniform encoding and handling. + * + *

Intentionally package-private to prevent extension outside of library. + * + * @param Subclass type. + */ +abstract class AbstractOptions { + + protected final InternalOptions options; + + AbstractOptions(InternalOptions options) { + this.options = options; + } + + abstract T self(InternalOptions options); + + public final T with(String key, String value) { + return with(key, encodeValue(value)); + } + + public final T with(String key, boolean value) { + return with(key, encodeValue(value)); + } + + public final T with(String key, long value) { + return with(key, encodeValue(value)); + } + + public final T with(String key, double value) { + return with(key, encodeValue(value)); + } + + public final T with(String key, Field value) { + return with(key, value.toProto()); + } + + protected final T with(String key, Value value) { + return self(options.with(key, value)); + } + + protected final T with(String key, String[] values) { + return self(options.with(key, Arrays.stream(values).map(PipelineUtils::encodeValue)::iterator)); + } + + protected final T with(String key, List expressions) { + return self(options.with(key, Lists.transform(expressions, FunctionUtils::exprToValue))); + } + + protected final T with(String key, AbstractOptions subSection) { + return self(options.with(key, subSection.options)); + } + + protected final T adding(AbstractOptions subSection) { + return self(options.adding(subSection)); + } + + public final T withSection(String key, RawOptions subSection) { + return with(key, subSection); + } + + final ImmutableMap toMap() { + return options.options; + } +} diff --git a/google-cloud-firestore/src/main/java/com/google/cloud/firestore/pipeline/stages/AddFields.java b/google-cloud-firestore/src/main/java/com/google/cloud/firestore/pipeline/stages/AddFields.java new file mode 100644 index 000000000..6488aaee7 --- /dev/null +++ b/google-cloud-firestore/src/main/java/com/google/cloud/firestore/pipeline/stages/AddFields.java @@ -0,0 +1,42 @@ +/* + * Copyright 2024 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.pipeline.stages; + +import static com.google.cloud.firestore.PipelineUtils.encodeValue; + +import com.google.api.core.InternalApi; +import com.google.cloud.firestore.pipeline.expressions.Expression; +import com.google.firestore.v1.Value; +import java.util.Collections; +import java.util.Map; + +@InternalApi +public final class AddFields extends Stage { + + private final Map fields; + + @InternalApi + public AddFields(Map fields) { + super("add_fields", InternalOptions.EMPTY); + this.fields = fields; + } + + @Override + Iterable toStageArgs() { + return Collections.singletonList(encodeValue(fields)); + } +} diff --git a/google-cloud-firestore/src/main/java/com/google/cloud/firestore/pipeline/stages/Aggregate.java b/google-cloud-firestore/src/main/java/com/google/cloud/firestore/pipeline/stages/Aggregate.java new file mode 100644 index 000000000..a4e64c870 --- /dev/null +++ b/google-cloud-firestore/src/main/java/com/google/cloud/firestore/pipeline/stages/Aggregate.java @@ -0,0 +1,90 @@ +/* + * Copyright 2024 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.pipeline.stages; + +import static com.google.cloud.firestore.PipelineUtils.encodeValue; + +import com.google.api.core.BetaApi; +import com.google.api.core.InternalApi; +import com.google.cloud.firestore.PipelineUtils; +import com.google.cloud.firestore.pipeline.expressions.AggregateFunction; +import com.google.cloud.firestore.pipeline.expressions.AliasedAggregate; +import com.google.cloud.firestore.pipeline.expressions.Expression; +import com.google.cloud.firestore.pipeline.expressions.Selectable; +import com.google.firestore.v1.Value; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; +import javax.annotation.Nonnull; + +@BetaApi +public final class Aggregate extends Stage { + + private final Map groups; + private final Map accumulators; + + @BetaApi + public Aggregate withGroups(String... fields) { + return new Aggregate( + PipelineUtils.fieldNamesToMap(fields), this.accumulators, new AggregateOptions()); + } + + @BetaApi + public Aggregate withGroups(Selectable... selectables) { + return new Aggregate( + PipelineUtils.selectablesToMap(selectables), this.accumulators, new AggregateOptions()); + } + + @BetaApi + public static Aggregate withAccumulators(AliasedAggregate... accumulators) { + Map accumulatorMap = new HashMap<>(); + for (AliasedAggregate accumulator : accumulators) { + if (accumulatorMap.containsKey(accumulator.getAlias())) { + throw new IllegalArgumentException( + "Duplicate alias or field name: " + accumulator.getAlias()); + } + accumulatorMap.put(accumulator.getAlias(), accumulator.getExpr()); + } + return new Aggregate(Collections.emptyMap(), accumulatorMap, new AggregateOptions()); + } + + @InternalApi + public Aggregate withOptions(@Nonnull AggregateOptions options) { + return new Aggregate(groups, accumulators, options); + } + + private Aggregate( + Map groups, + Map accumulators, + AggregateOptions options) { + super("aggregate", options.options); + if (accumulators.isEmpty()) { + throw new IllegalArgumentException( + "Must specify at least one accumulator for aggregate() stage. There is a distinct() stage" + + " if only distinct group values are needed."); + } + + this.groups = groups; + this.accumulators = accumulators; + } + + @Override + Iterable toStageArgs() { + return Arrays.asList(encodeValue(accumulators), encodeValue(groups)); + } +} diff --git a/google-cloud-firestore/src/main/java/com/google/cloud/firestore/pipeline/stages/AggregateHints.java b/google-cloud-firestore/src/main/java/com/google/cloud/firestore/pipeline/stages/AggregateHints.java new file mode 100644 index 000000000..8538ad2d4 --- /dev/null +++ b/google-cloud-firestore/src/main/java/com/google/cloud/firestore/pipeline/stages/AggregateHints.java @@ -0,0 +1,37 @@ +/* + * Copyright 2024 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.pipeline.stages; + +public final class AggregateHints extends AbstractOptions { + + public AggregateHints() { + this(InternalOptions.EMPTY); + } + + AggregateHints(InternalOptions options) { + super(options); + } + + @Override + AggregateHints self(InternalOptions options) { + return new AggregateHints(options); + } + + public AggregateHints withForceStreamableEnabled() { + return with("force_streamable", true); + } +} diff --git a/google-cloud-firestore/src/main/java/com/google/cloud/firestore/pipeline/stages/AggregateOptions.java b/google-cloud-firestore/src/main/java/com/google/cloud/firestore/pipeline/stages/AggregateOptions.java new file mode 100644 index 000000000..7cb7d7309 --- /dev/null +++ b/google-cloud-firestore/src/main/java/com/google/cloud/firestore/pipeline/stages/AggregateOptions.java @@ -0,0 +1,37 @@ +/* + * Copyright 2024 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.pipeline.stages; + +public final class AggregateOptions extends AbstractOptions { + + public AggregateOptions() { + this(InternalOptions.EMPTY); + } + + AggregateOptions(InternalOptions options) { + super(options); + } + + @Override + AggregateOptions self(InternalOptions options) { + return new AggregateOptions(options); + } + + public AggregateOptions withHints(AggregateHints hints) { + return adding(hints); + } +} diff --git a/google-cloud-firestore/src/main/java/com/google/cloud/firestore/pipeline/stages/Collection.java b/google-cloud-firestore/src/main/java/com/google/cloud/firestore/pipeline/stages/Collection.java new file mode 100644 index 000000000..0d049b0ee --- /dev/null +++ b/google-cloud-firestore/src/main/java/com/google/cloud/firestore/pipeline/stages/Collection.java @@ -0,0 +1,46 @@ +/* + * Copyright 2024 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.pipeline.stages; + +import com.google.api.core.InternalApi; +import com.google.firestore.v1.Value; +import java.util.Collections; +import javax.annotation.Nonnull; + +@InternalApi +public final class Collection extends Stage { + + @Nonnull private final String path; + + public Collection(@Nonnull String path, CollectionOptions options) { + super("collection", options.options); + if (!path.startsWith("/")) { + this.path = "/" + path; + } else { + this.path = path; + } + } + + public Collection withOptions(CollectionOptions options) { + return new Collection(path, options); + } + + @Override + Iterable toStageArgs() { + return Collections.singleton(Value.newBuilder().setReferenceValue(path).build()); + } +} diff --git a/google-cloud-firestore/src/main/java/com/google/cloud/firestore/pipeline/stages/CollectionGroup.java b/google-cloud-firestore/src/main/java/com/google/cloud/firestore/pipeline/stages/CollectionGroup.java new file mode 100644 index 000000000..f3535230a --- /dev/null +++ b/google-cloud-firestore/src/main/java/com/google/cloud/firestore/pipeline/stages/CollectionGroup.java @@ -0,0 +1,45 @@ +/* + * Copyright 2024 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.pipeline.stages; + +import static com.google.cloud.firestore.PipelineUtils.encodeValue; + +import com.google.api.core.InternalApi; +import com.google.common.collect.ImmutableList; +import com.google.firestore.v1.Value; + +@InternalApi +public final class CollectionGroup extends Stage { + + private final String collectionId; + + @InternalApi + public CollectionGroup(String collectionId, CollectionGroupOptions options) { + super("collection_group", options.options); + this.collectionId = collectionId; + } + + public CollectionGroup withOptions(CollectionGroupOptions options) { + return new CollectionGroup(collectionId, options); + } + + @Override + Iterable toStageArgs() { + return ImmutableList.of( + Value.newBuilder().setReferenceValue("").build(), encodeValue(collectionId)); + } +} diff --git a/google-cloud-firestore/src/main/java/com/google/cloud/firestore/pipeline/stages/CollectionGroupOptions.java b/google-cloud-firestore/src/main/java/com/google/cloud/firestore/pipeline/stages/CollectionGroupOptions.java new file mode 100644 index 000000000..e42bef68b --- /dev/null +++ b/google-cloud-firestore/src/main/java/com/google/cloud/firestore/pipeline/stages/CollectionGroupOptions.java @@ -0,0 +1,37 @@ +/* + * Copyright 2024 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.pipeline.stages; + +public final class CollectionGroupOptions extends AbstractOptions { + + public CollectionGroupOptions() { + this(InternalOptions.EMPTY); + } + + CollectionGroupOptions(InternalOptions options) { + super(options); + } + + @Override + CollectionGroupOptions self(InternalOptions options) { + return new CollectionGroupOptions(options); + } + + public CollectionGroupOptions withHints(CollectionHints hints) { + return with("hints", hints); + } +} diff --git a/google-cloud-firestore/src/main/java/com/google/cloud/firestore/pipeline/stages/CollectionHints.java b/google-cloud-firestore/src/main/java/com/google/cloud/firestore/pipeline/stages/CollectionHints.java new file mode 100644 index 000000000..5f6acd59c --- /dev/null +++ b/google-cloud-firestore/src/main/java/com/google/cloud/firestore/pipeline/stages/CollectionHints.java @@ -0,0 +1,41 @@ +/* + * Copyright 2024 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.pipeline.stages; + +public final class CollectionHints extends AbstractOptions { + + public CollectionHints() { + this(InternalOptions.EMPTY); + } + + CollectionHints(InternalOptions options) { + super(options); + } + + @Override + CollectionHints self(InternalOptions options) { + return new CollectionHints(options); + } + + public CollectionHints withForceIndex(String value) { + return with("force_index", value); + } + + public CollectionHints withIgnoreIndexFields(String... values) { + return with("ignore_index_fields", values); + } +} diff --git a/google-cloud-firestore/src/main/java/com/google/cloud/firestore/pipeline/stages/CollectionOptions.java b/google-cloud-firestore/src/main/java/com/google/cloud/firestore/pipeline/stages/CollectionOptions.java new file mode 100644 index 000000000..73dea074e --- /dev/null +++ b/google-cloud-firestore/src/main/java/com/google/cloud/firestore/pipeline/stages/CollectionOptions.java @@ -0,0 +1,37 @@ +/* + * Copyright 2024 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.pipeline.stages; + +public final class CollectionOptions extends AbstractOptions { + + public CollectionOptions() { + super(InternalOptions.EMPTY); + } + + CollectionOptions(InternalOptions options) { + super(options); + } + + @Override + CollectionOptions self(InternalOptions options) { + return new CollectionOptions(options); + } + + public CollectionOptions withHints(CollectionHints hints) { + return adding(hints); + } +} diff --git a/google-cloud-firestore/src/main/java/com/google/cloud/firestore/pipeline/stages/Database.java b/google-cloud-firestore/src/main/java/com/google/cloud/firestore/pipeline/stages/Database.java new file mode 100644 index 000000000..121cde8f5 --- /dev/null +++ b/google-cloud-firestore/src/main/java/com/google/cloud/firestore/pipeline/stages/Database.java @@ -0,0 +1,35 @@ +/* + * Copyright 2024 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.pipeline.stages; + +import com.google.api.core.InternalApi; +import com.google.firestore.v1.Value; +import java.util.Collections; + +@InternalApi +public final class Database extends Stage { + + @InternalApi + public Database() { + super("database", InternalOptions.EMPTY); + } + + @Override + Iterable toStageArgs() { + return Collections.emptyList(); + } +} diff --git a/google-cloud-firestore/src/main/java/com/google/cloud/firestore/pipeline/stages/Distinct.java b/google-cloud-firestore/src/main/java/com/google/cloud/firestore/pipeline/stages/Distinct.java new file mode 100644 index 000000000..58f54c019 --- /dev/null +++ b/google-cloud-firestore/src/main/java/com/google/cloud/firestore/pipeline/stages/Distinct.java @@ -0,0 +1,43 @@ +/* + * Copyright 2024 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.pipeline.stages; + +import static com.google.cloud.firestore.PipelineUtils.encodeValue; + +import com.google.api.core.BetaApi; +import com.google.api.core.InternalApi; +import com.google.cloud.firestore.pipeline.expressions.Expression; +import com.google.firestore.v1.Value; +import java.util.Collections; +import java.util.Map; + +@BetaApi +public final class Distinct extends Stage { + + private final Map groups; + + @InternalApi + public Distinct(Map groups) { + super("distinct", InternalOptions.EMPTY); + this.groups = groups; + } + + @Override + Iterable toStageArgs() { + return Collections.singletonList(encodeValue(groups)); + } +} diff --git a/google-cloud-firestore/src/main/java/com/google/cloud/firestore/pipeline/stages/Documents.java b/google-cloud-firestore/src/main/java/com/google/cloud/firestore/pipeline/stages/Documents.java new file mode 100644 index 000000000..93dd2322e --- /dev/null +++ b/google-cloud-firestore/src/main/java/com/google/cloud/firestore/pipeline/stages/Documents.java @@ -0,0 +1,48 @@ +/* + * Copyright 2024 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.pipeline.stages; + +import com.google.api.core.InternalApi; +import com.google.cloud.firestore.DocumentReference; +import com.google.common.collect.Iterables; +import com.google.firestore.v1.Value; +import java.util.Arrays; +import java.util.List; +import java.util.stream.Collectors; + +@InternalApi +public final class Documents extends Stage { + + private List documents; + + @InternalApi + Documents(List documents) { + super("documents", InternalOptions.EMPTY); + this.documents = documents; + } + + @InternalApi + public static Documents of(DocumentReference... documents) { + return new Documents( + Arrays.stream(documents).map(doc -> "/" + doc.getPath()).collect(Collectors.toList())); + } + + @Override + Iterable toStageArgs() { + return Iterables.transform(documents, doc -> Value.newBuilder().setReferenceValue(doc).build()); + } +} diff --git a/google-cloud-firestore/src/main/java/com/google/cloud/firestore/pipeline/stages/ExplainOptions.java b/google-cloud-firestore/src/main/java/com/google/cloud/firestore/pipeline/stages/ExplainOptions.java new file mode 100644 index 000000000..edafba572 --- /dev/null +++ b/google-cloud-firestore/src/main/java/com/google/cloud/firestore/pipeline/stages/ExplainOptions.java @@ -0,0 +1,55 @@ +/* + * Copyright 2024 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.pipeline.stages; + +import com.google.cloud.firestore.PipelineUtils; +import com.google.firestore.v1.Value; + +public final class ExplainOptions extends AbstractOptions { + + public static class ExecutionMode { + public static final ExecutionMode EXPLAIN = new ExecutionMode("explain"); + public static final ExecutionMode ANALYZE = new ExecutionMode("analyze"); + + private final Value value; + + private ExecutionMode(String format) { + this.value = PipelineUtils.encodeValue(format); + } + + public Value toProto() { + return value; + } + } + + public ExplainOptions() { + super(InternalOptions.EMPTY); + } + + ExplainOptions(InternalOptions options) { + super(options); + } + + @Override + ExplainOptions self(InternalOptions options) { + return new ExplainOptions(options); + } + + public ExplainOptions withExecutionMode(ExecutionMode mode) { + return with("mode", mode.toProto()); + } +} diff --git a/google-cloud-firestore/src/main/java/com/google/cloud/firestore/pipeline/stages/FindNearest.java b/google-cloud-firestore/src/main/java/com/google/cloud/firestore/pipeline/stages/FindNearest.java new file mode 100644 index 000000000..6cefeffb4 --- /dev/null +++ b/google-cloud-firestore/src/main/java/com/google/cloud/firestore/pipeline/stages/FindNearest.java @@ -0,0 +1,72 @@ +/* + * Copyright 2024 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.pipeline.stages; + +import static com.google.cloud.firestore.PipelineUtils.encodeValue; + +import com.google.api.core.BetaApi; +import com.google.api.core.InternalApi; +import com.google.cloud.firestore.VectorValue; +import com.google.cloud.firestore.pipeline.expressions.Expression; +import com.google.common.collect.ImmutableList; +import com.google.firestore.v1.Value; + +@BetaApi +public final class FindNearest extends Stage { + + public static final class DistanceMeasure { + + final String protoString; + + private DistanceMeasure(String protoString) { + this.protoString = protoString; + } + + public static final DistanceMeasure EUCLIDEAN = new DistanceMeasure("euclidean"); + public static final DistanceMeasure COSINE = new DistanceMeasure("cosine"); + public static final DistanceMeasure DOT_PRODUCT = new DistanceMeasure("dot_product"); + + public static DistanceMeasure generic(String name) { + return new DistanceMeasure(name); + } + + Value toProto() { + return encodeValue(protoString); + } + } + + private final Expression property; + private final VectorValue vector; + private final DistanceMeasure distanceMeasure; + + @InternalApi + public FindNearest( + Expression property, + VectorValue vector, + DistanceMeasure distanceMeasure, + FindNearestOptions options) { + super("find_nearest", options.options); + this.property = property; + this.vector = vector; + this.distanceMeasure = distanceMeasure; + } + + @Override + Iterable toStageArgs() { + return ImmutableList.of(encodeValue(property), encodeValue(vector), distanceMeasure.toProto()); + } +} diff --git a/google-cloud-firestore/src/main/java/com/google/cloud/firestore/pipeline/stages/FindNearestOptions.java b/google-cloud-firestore/src/main/java/com/google/cloud/firestore/pipeline/stages/FindNearestOptions.java new file mode 100644 index 000000000..e876517c5 --- /dev/null +++ b/google-cloud-firestore/src/main/java/com/google/cloud/firestore/pipeline/stages/FindNearestOptions.java @@ -0,0 +1,51 @@ +/* + * Copyright 2024 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.pipeline.stages; + +import static com.google.cloud.firestore.pipeline.expressions.Expression.field; + +import com.google.api.core.BetaApi; +import com.google.cloud.firestore.pipeline.expressions.Field; + +@BetaApi +public final class FindNearestOptions extends AbstractOptions { + + public FindNearestOptions() { + this(InternalOptions.EMPTY); + } + + private FindNearestOptions(InternalOptions options) { + super(options); + } + + @Override + FindNearestOptions self(InternalOptions options) { + return new FindNearestOptions(options); + } + + public FindNearestOptions withLimit(long limit) { + return with("limit", limit); + } + + public FindNearestOptions withDistanceField(Field distanceField) { + return with("distance_field", distanceField); + } + + public FindNearestOptions withDistanceField(String distanceField) { + return withDistanceField(field(distanceField)); + } +} diff --git a/google-cloud-firestore/src/main/java/com/google/cloud/firestore/pipeline/stages/InternalOptions.java b/google-cloud-firestore/src/main/java/com/google/cloud/firestore/pipeline/stages/InternalOptions.java new file mode 100644 index 000000000..300e352e8 --- /dev/null +++ b/google-cloud-firestore/src/main/java/com/google/cloud/firestore/pipeline/stages/InternalOptions.java @@ -0,0 +1,78 @@ +/* + * Copyright 2024 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.pipeline.stages; + +import com.google.common.collect.ImmutableMap; +import com.google.firestore.v1.ArrayValue; +import com.google.firestore.v1.MapValue; +import com.google.firestore.v1.Value; + +/** + * Wither style Key/Value options object. + * + *

Basic `wither` functionality built upon `ImmutableMap`. Exposes methods to + * construct, augment, and encode Kay/Value pairs. The wrapped collection `ImmutableMap` is an implementation detail, not to be exposed, since more efficient implementations are + * possible. + */ +final class InternalOptions { + + public static final InternalOptions EMPTY = new InternalOptions(ImmutableMap.of()); + + final ImmutableMap options; + + InternalOptions(ImmutableMap options) { + this.options = options; + } + + public static InternalOptions of(String key, Value value) { + return new InternalOptions(ImmutableMap.of(key, value)); + } + + InternalOptions with(String key, Value value) { + ImmutableMap.Builder builder = + ImmutableMap.builderWithExpectedSize(options.size() + 1); + builder.putAll(options); + builder.put(key, value); + return new InternalOptions(builder.buildKeepingLast()); + } + + InternalOptions with(String key, Iterable values) { + ArrayValue arrayValue = ArrayValue.newBuilder().addAllValues(values).build(); + return with(key, Value.newBuilder().setArrayValue(arrayValue).build()); + } + + InternalOptions with(String key, InternalOptions value) { + return with(key, value.toValue()); + } + + InternalOptions adding(AbstractOptions value) { + ImmutableMap.Builder builder = + ImmutableMap.builderWithExpectedSize(options.size() + value.toMap().size()); + builder.putAll(options); + for (ImmutableMap.Entry entry : value.toMap().entrySet()) { + builder.put(entry.getKey(), entry.getValue()); + } + + return new InternalOptions(builder.buildKeepingLast()); + } + + private Value toValue() { + MapValue mapValue = MapValue.newBuilder().putAllFields(options).build(); + return Value.newBuilder().setMapValue(mapValue).build(); + } +} diff --git a/google-cloud-firestore/src/main/java/com/google/cloud/firestore/pipeline/stages/Limit.java b/google-cloud-firestore/src/main/java/com/google/cloud/firestore/pipeline/stages/Limit.java new file mode 100644 index 000000000..8723b980c --- /dev/null +++ b/google-cloud-firestore/src/main/java/com/google/cloud/firestore/pipeline/stages/Limit.java @@ -0,0 +1,40 @@ +/* + * Copyright 2024 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.pipeline.stages; + +import static com.google.cloud.firestore.PipelineUtils.encodeValue; + +import com.google.api.core.InternalApi; +import com.google.firestore.v1.Value; +import java.util.Collections; + +@InternalApi +public final class Limit extends Stage { + + private final int limit; + + @InternalApi + public Limit(int limit) { + super("limit", InternalOptions.EMPTY); + this.limit = limit; + } + + @Override + Iterable toStageArgs() { + return Collections.singletonList(encodeValue(limit)); + } +} diff --git a/google-cloud-firestore/src/main/java/com/google/cloud/firestore/pipeline/stages/Offset.java b/google-cloud-firestore/src/main/java/com/google/cloud/firestore/pipeline/stages/Offset.java new file mode 100644 index 000000000..63a96812a --- /dev/null +++ b/google-cloud-firestore/src/main/java/com/google/cloud/firestore/pipeline/stages/Offset.java @@ -0,0 +1,40 @@ +/* + * Copyright 2024 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.pipeline.stages; + +import static com.google.cloud.firestore.PipelineUtils.encodeValue; + +import com.google.api.core.InternalApi; +import com.google.firestore.v1.Value; +import java.util.Collections; + +@InternalApi +public final class Offset extends Stage { + + private final int offset; + + @InternalApi + public Offset(int offset) { + super("offset", InternalOptions.EMPTY); + this.offset = offset; + } + + @Override + Iterable toStageArgs() { + return Collections.singletonList(encodeValue(offset)); + } +} diff --git a/google-cloud-firestore/src/main/java/com/google/cloud/firestore/pipeline/stages/PipelineExecuteOptions.java b/google-cloud-firestore/src/main/java/com/google/cloud/firestore/pipeline/stages/PipelineExecuteOptions.java new file mode 100644 index 000000000..754215e67 --- /dev/null +++ b/google-cloud-firestore/src/main/java/com/google/cloud/firestore/pipeline/stages/PipelineExecuteOptions.java @@ -0,0 +1,41 @@ +/* + * Copyright 2024 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.pipeline.stages; + +public final class PipelineExecuteOptions extends AbstractOptions { + + public PipelineExecuteOptions() { + super(InternalOptions.EMPTY); + } + + PipelineExecuteOptions(InternalOptions options) { + super(options); + } + + @Override + PipelineExecuteOptions self(InternalOptions options) { + return new PipelineExecuteOptions(options); + } + + public PipelineExecuteOptions withExplainOptions(ExplainOptions options) { + return with("explain_options", options); + } + + public PipelineExecuteOptions withIndexMode(String indexMode) { + return with("index_mode", indexMode); + } +} diff --git a/google-cloud-firestore/src/main/java/com/google/cloud/firestore/pipeline/stages/RawOptions.java b/google-cloud-firestore/src/main/java/com/google/cloud/firestore/pipeline/stages/RawOptions.java new file mode 100644 index 000000000..1e8a1f0c2 --- /dev/null +++ b/google-cloud-firestore/src/main/java/com/google/cloud/firestore/pipeline/stages/RawOptions.java @@ -0,0 +1,57 @@ +/* + * Copyright 2024 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.pipeline.stages; + +import static com.google.cloud.firestore.PipelineUtils.encodeValue; + +import com.google.cloud.firestore.pipeline.expressions.Field; + +public final class RawOptions extends AbstractOptions { + + public RawOptions() { + this(InternalOptions.EMPTY); + } + + public static RawOptions of(String key, String value) { + return new RawOptions(InternalOptions.of(key, encodeValue(value))); + } + + public static RawOptions of(String key, boolean value) { + return new RawOptions(InternalOptions.of(key, encodeValue(value))); + } + + public static RawOptions of(String key, long value) { + return new RawOptions(InternalOptions.of(key, encodeValue(value))); + } + + public static RawOptions of(String key, double value) { + return new RawOptions(InternalOptions.of(key, encodeValue(value))); + } + + public static RawOptions of(String key, Field value) { + return new RawOptions(InternalOptions.of(key, value.toProto())); + } + + RawOptions(InternalOptions options) { + super(options); + } + + @Override + protected RawOptions self(InternalOptions options) { + return new RawOptions(options); + } +} diff --git a/google-cloud-firestore/src/main/java/com/google/cloud/firestore/pipeline/stages/RawStage.java b/google-cloud-firestore/src/main/java/com/google/cloud/firestore/pipeline/stages/RawStage.java new file mode 100644 index 000000000..d25ec0495 --- /dev/null +++ b/google-cloud-firestore/src/main/java/com/google/cloud/firestore/pipeline/stages/RawStage.java @@ -0,0 +1,78 @@ +/* + * Copyright 2024 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.pipeline.stages; + +import com.google.api.core.InternalApi; +import com.google.cloud.firestore.PipelineUtils; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.Iterables; +import com.google.firestore.v1.Value; +import java.util.Arrays; +import java.util.List; +import javax.annotation.Nonnull; + +/** + * Adds a stage to the pipeline by specifying the stage name as an argument. This does not offer any + * type safety on the stage params and requires the caller to know the order (and optionally names) + * of parameters accepted by the stage. + * + *

This class provides a way to call stages that are supported by the Firestore backend but that + * are not implemented in the SDK version being used. + */ +public final class RawStage extends Stage { + + private final List arguments; + + private RawStage( + @Nonnull String name, @Nonnull List arguments, @Nonnull RawOptions options) { + super(name, options.options); + this.arguments = arguments; + } + + /** + * Specify name of stage + * + * @param name The unique name of the stage to add. + * @return A new {@code RawStage} for the specified stage name. + */ + @Nonnull + public static RawStage ofName(@Nonnull String name) { + return new RawStage(name, ImmutableList.of(), new RawOptions()); + } + + /** + * Specify arguments to stage. + * + * @param arguments A list of ordered parameters to configure the stage's behavior. + * @return {@code RawStage} with specified parameters. + */ + @Nonnull + public RawStage withArguments(@Nonnull Object... arguments) { + return new RawStage(name, Arrays.asList(arguments), new RawOptions(options)); + } + + @Nonnull + public RawStage withOptions(@Nonnull RawOptions options) { + return new RawStage(name, arguments, options); + } + + @Override + @InternalApi + Iterable toStageArgs() { + return Iterables.transform(arguments, PipelineUtils::encodeValue); + } +} diff --git a/google-cloud-firestore/src/main/java/com/google/cloud/firestore/pipeline/stages/RemoveFields.java b/google-cloud-firestore/src/main/java/com/google/cloud/firestore/pipeline/stages/RemoveFields.java new file mode 100644 index 000000000..613f1bf0e --- /dev/null +++ b/google-cloud-firestore/src/main/java/com/google/cloud/firestore/pipeline/stages/RemoveFields.java @@ -0,0 +1,41 @@ +/* + * Copyright 2024 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.pipeline.stages; + +import com.google.api.core.InternalApi; +import com.google.cloud.firestore.PipelineUtils; +import com.google.cloud.firestore.pipeline.expressions.Field; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.Iterables; +import com.google.firestore.v1.Value; + +@InternalApi +public final class RemoveFields extends Stage { + + private final ImmutableList fields; + + @InternalApi + public RemoveFields(ImmutableList fields) { + super("remove_fields", InternalOptions.EMPTY); + this.fields = fields; + } + + @Override + Iterable toStageArgs() { + return Iterables.transform(fields, PipelineUtils::encodeValue); + } +} diff --git a/google-cloud-firestore/src/main/java/com/google/cloud/firestore/pipeline/stages/ReplaceWith.java b/google-cloud-firestore/src/main/java/com/google/cloud/firestore/pipeline/stages/ReplaceWith.java new file mode 100644 index 000000000..1fe58d1ac --- /dev/null +++ b/google-cloud-firestore/src/main/java/com/google/cloud/firestore/pipeline/stages/ReplaceWith.java @@ -0,0 +1,57 @@ +/* + * Copyright 2024 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.pipeline.stages; + +import static com.google.cloud.firestore.PipelineUtils.encodeValue; + +import com.google.cloud.firestore.pipeline.expressions.Expression; +import com.google.common.collect.ImmutableList; +import com.google.firestore.v1.Value; +import javax.annotation.Nonnull; + +public class ReplaceWith extends Stage { + + private final Expression expr; + private final Mode mode; + + public enum Mode { + FULL_REPLACE(Value.newBuilder().setStringValue("full_replace").build()), + MERGE_PREFER_NEXT(Value.newBuilder().setStringValue("merge_prefer_nest").build()), + MERGE_PREFER_PARENT(Value.newBuilder().setStringValue("merge_prefer_parent").build()); + + public final Value value; + + Mode(Value value) { + this.value = value; + } + } + + public ReplaceWith(@Nonnull Expression field) { + this(field, Mode.FULL_REPLACE); + } + + public ReplaceWith(@Nonnull Expression expr, @Nonnull Mode mode) { + super("replace_with", InternalOptions.EMPTY); + this.expr = expr; + this.mode = mode; + } + + @Override + Iterable toStageArgs() { + return ImmutableList.of(encodeValue(expr), mode.value); + } +} diff --git a/google-cloud-firestore/src/main/java/com/google/cloud/firestore/pipeline/stages/Sample.java b/google-cloud-firestore/src/main/java/com/google/cloud/firestore/pipeline/stages/Sample.java new file mode 100644 index 000000000..d4a5bd22b --- /dev/null +++ b/google-cloud-firestore/src/main/java/com/google/cloud/firestore/pipeline/stages/Sample.java @@ -0,0 +1,69 @@ +/* + * Copyright 2024 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.pipeline.stages; + +import static com.google.cloud.firestore.PipelineUtils.encodeValue; + +import com.google.api.core.BetaApi; +import com.google.api.core.InternalApi; +import com.google.common.collect.ImmutableList; +import com.google.firestore.v1.Value; +import javax.annotation.Nonnull; + +public final class Sample extends Stage { + + private final Number size; + private final Mode mode; + + public enum Mode { + DOCUMENTS(encodeValue("documents")), + PERCENT(encodeValue("percent")); + + public final Value value; + + Mode(Value value) { + this.value = value; + } + } + + @BetaApi + public static Sample withPercentage(double percentage) { + return new Sample(percentage, Mode.PERCENT, new SampleOptions()); + } + + @BetaApi + public static Sample withDocLimit(int documents) { + return new Sample(documents, Mode.DOCUMENTS, new SampleOptions()); + } + + @BetaApi + public Sample withOptions(@Nonnull SampleOptions options) { + return new Sample(size, mode, options); + } + + @InternalApi + private Sample(Number size, Mode mode, SampleOptions options) { + super("sample", options.options); + this.size = size; + this.mode = mode; + } + + @Override + Iterable toStageArgs() { + return ImmutableList.of(encodeValue(size), mode.value); + } +} diff --git a/google-cloud-firestore/src/main/java/com/google/cloud/firestore/pipeline/stages/SampleOptions.java b/google-cloud-firestore/src/main/java/com/google/cloud/firestore/pipeline/stages/SampleOptions.java new file mode 100644 index 000000000..2e88b2802 --- /dev/null +++ b/google-cloud-firestore/src/main/java/com/google/cloud/firestore/pipeline/stages/SampleOptions.java @@ -0,0 +1,33 @@ +/* + * Copyright 2024 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.pipeline.stages; + +public final class SampleOptions extends AbstractOptions { + + public SampleOptions() { + this(InternalOptions.EMPTY); + } + + SampleOptions(InternalOptions options) { + super(options); + } + + @Override + SampleOptions self(InternalOptions options) { + return new SampleOptions(options); + } +} diff --git a/google-cloud-firestore/src/main/java/com/google/cloud/firestore/pipeline/stages/Select.java b/google-cloud-firestore/src/main/java/com/google/cloud/firestore/pipeline/stages/Select.java new file mode 100644 index 000000000..1017d90d3 --- /dev/null +++ b/google-cloud-firestore/src/main/java/com/google/cloud/firestore/pipeline/stages/Select.java @@ -0,0 +1,42 @@ +/* + * Copyright 2024 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.pipeline.stages; + +import static com.google.cloud.firestore.PipelineUtils.encodeValue; + +import com.google.api.core.InternalApi; +import com.google.cloud.firestore.pipeline.expressions.Expression; +import com.google.firestore.v1.Value; +import java.util.Collections; +import java.util.Map; + +@InternalApi +public final class Select extends Stage { + + private final Map projections; + + @InternalApi + public Select(Map projections) { + super("select", InternalOptions.EMPTY); + this.projections = projections; + } + + @Override + Iterable toStageArgs() { + return Collections.singletonList(encodeValue(projections)); + } +} diff --git a/google-cloud-firestore/src/main/java/com/google/cloud/firestore/pipeline/stages/Sort.java b/google-cloud-firestore/src/main/java/com/google/cloud/firestore/pipeline/stages/Sort.java new file mode 100644 index 000000000..c89266118 --- /dev/null +++ b/google-cloud-firestore/src/main/java/com/google/cloud/firestore/pipeline/stages/Sort.java @@ -0,0 +1,41 @@ +/* + * Copyright 2024 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.pipeline.stages; + +import com.google.api.core.InternalApi; +import com.google.cloud.firestore.pipeline.expressions.Ordering; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.Iterables; +import com.google.firestore.v1.Value; +import java.util.List; + +public final class Sort extends Stage { + + private static final String name = "sort"; + private final List orders; + + @InternalApi + public Sort(ImmutableList orders) { + super("sort", InternalOptions.EMPTY); + this.orders = orders; + } + + @Override + Iterable toStageArgs() { + return Iterables.transform(orders, Ordering::toProto); + } +} diff --git a/google-cloud-firestore/src/main/java/com/google/cloud/firestore/pipeline/stages/Stage.java b/google-cloud-firestore/src/main/java/com/google/cloud/firestore/pipeline/stages/Stage.java new file mode 100644 index 000000000..8f82195f7 --- /dev/null +++ b/google-cloud-firestore/src/main/java/com/google/cloud/firestore/pipeline/stages/Stage.java @@ -0,0 +1,43 @@ +/* + * Copyright 2024 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.pipeline.stages; + +import com.google.firestore.v1.Pipeline; +import com.google.firestore.v1.Value; + +/** Parent to all stages. */ +public abstract class Stage { + + protected final String name; + final InternalOptions options; + + /** Constructor is package-private to prevent extension. */ + Stage(String name, InternalOptions options) { + this.name = name; + this.options = options; + } + + final Pipeline.Stage toStageProto() { + return Pipeline.Stage.newBuilder() + .setName(name) + .addAllArgs(toStageArgs()) + .putAllOptions(options.options) + .build(); + } + + abstract Iterable toStageArgs(); +} diff --git a/google-cloud-firestore/src/main/java/com/google/cloud/firestore/pipeline/stages/StageUtils.java b/google-cloud-firestore/src/main/java/com/google/cloud/firestore/pipeline/stages/StageUtils.java new file mode 100644 index 000000000..5381daa50 --- /dev/null +++ b/google-cloud-firestore/src/main/java/com/google/cloud/firestore/pipeline/stages/StageUtils.java @@ -0,0 +1,35 @@ +/* + * Copyright 2024 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.pipeline.stages; + +import com.google.api.core.InternalApi; +import com.google.common.collect.ImmutableMap; +import com.google.firestore.v1.Value; + +@InternalApi +public final class StageUtils { + @InternalApi + public static com.google.firestore.v1.Pipeline.Stage toStageProto(Stage stage) { + return stage.toStageProto(); + } + + @SuppressWarnings("ReferencesHidden") + @InternalApi + public static ImmutableMap toMap(AbstractOptions options) { + return options.options.options; + } +} diff --git a/google-cloud-firestore/src/main/java/com/google/cloud/firestore/pipeline/stages/Union.java b/google-cloud-firestore/src/main/java/com/google/cloud/firestore/pipeline/stages/Union.java new file mode 100644 index 000000000..d54a650f4 --- /dev/null +++ b/google-cloud-firestore/src/main/java/com/google/cloud/firestore/pipeline/stages/Union.java @@ -0,0 +1,36 @@ +/* + * Copyright 2024 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.pipeline.stages; + +import com.google.cloud.firestore.Pipeline; +import com.google.firestore.v1.Value; +import java.util.Collections; + +public final class Union extends Stage { + + private final Pipeline other; + + public Union(Pipeline other) { + super("union", InternalOptions.EMPTY); + this.other = other; + } + + @Override + Iterable toStageArgs() { + return Collections.singletonList(other.toProtoValue()); + } +} diff --git a/google-cloud-firestore/src/main/java/com/google/cloud/firestore/pipeline/stages/Unnest.java b/google-cloud-firestore/src/main/java/com/google/cloud/firestore/pipeline/stages/Unnest.java new file mode 100644 index 000000000..d7b70a828 --- /dev/null +++ b/google-cloud-firestore/src/main/java/com/google/cloud/firestore/pipeline/stages/Unnest.java @@ -0,0 +1,62 @@ +/* + * Copyright 2024 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.pipeline.stages; + +import static com.google.cloud.firestore.PipelineUtils.encodeValue; +import static com.google.cloud.firestore.pipeline.expressions.Expression.field; + +import com.google.cloud.firestore.pipeline.expressions.AliasedExpression; +import com.google.cloud.firestore.pipeline.expressions.Expression; +import com.google.cloud.firestore.pipeline.expressions.Field; +import com.google.cloud.firestore.pipeline.expressions.Selectable; +import com.google.common.collect.ImmutableList; +import com.google.firestore.v1.Value; +import javax.annotation.Nonnull; + +public final class Unnest extends Stage { + + private final Expression expr; + private final Field alias; + + public Unnest(@Nonnull Field field, @Nonnull String alias) { + super("unnest", InternalOptions.EMPTY); + this.expr = field; + this.alias = field(alias); + } + + public Unnest(@Nonnull Field field, @Nonnull String alias, @Nonnull UnnestOptions options) { + super("unnest", options.options); + this.expr = field; + this.alias = field(alias); + } + + public Unnest(@Nonnull Selectable field) { + super("unnest", InternalOptions.EMPTY); + if (field instanceof AliasedExpression) { + this.expr = ((AliasedExpression) field).getExpr(); + this.alias = field(((AliasedExpression) field).getAlias()); + } else { + this.expr = (Field) field; + this.alias = (Field) field; + } + } + + @Override + Iterable toStageArgs() { + return ImmutableList.of(encodeValue(expr), encodeValue(alias)); + } +} diff --git a/google-cloud-firestore/src/main/java/com/google/cloud/firestore/pipeline/stages/UnnestOptions.java b/google-cloud-firestore/src/main/java/com/google/cloud/firestore/pipeline/stages/UnnestOptions.java new file mode 100644 index 000000000..1b8e267f0 --- /dev/null +++ b/google-cloud-firestore/src/main/java/com/google/cloud/firestore/pipeline/stages/UnnestOptions.java @@ -0,0 +1,40 @@ +/* + * Copyright 2024 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.pipeline.stages; + +import com.google.firestore.v1.Value; +import javax.annotation.Nonnull; + +public final class UnnestOptions extends AbstractOptions { + + public UnnestOptions() { + this(InternalOptions.EMPTY); + } + + public UnnestOptions withIndexField(@Nonnull String indexField) { + return with("index_field", Value.newBuilder().setFieldReferenceValue(indexField).build()); + } + + @Override + UnnestOptions self(InternalOptions options) { + return new UnnestOptions(options); + } + + private UnnestOptions(InternalOptions options) { + super(options); + } +} diff --git a/google-cloud-firestore/src/main/java/com/google/cloud/firestore/pipeline/stages/Where.java b/google-cloud-firestore/src/main/java/com/google/cloud/firestore/pipeline/stages/Where.java new file mode 100644 index 000000000..d6626f7cc --- /dev/null +++ b/google-cloud-firestore/src/main/java/com/google/cloud/firestore/pipeline/stages/Where.java @@ -0,0 +1,41 @@ +/* + * Copyright 2024 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.pipeline.stages; + +import static com.google.cloud.firestore.PipelineUtils.encodeValue; + +import com.google.api.core.InternalApi; +import com.google.cloud.firestore.pipeline.expressions.BooleanExpression; +import com.google.firestore.v1.Value; +import java.util.Collections; + +@InternalApi +public final class Where extends Stage { + + private final BooleanExpression condition; + + @InternalApi + public Where(BooleanExpression condition) { + super("where", InternalOptions.EMPTY); + this.condition = condition; + } + + @Override + Iterable toStageArgs() { + return Collections.singletonList(encodeValue(condition)); + } +} diff --git a/google-cloud-firestore/src/main/java/com/google/cloud/firestore/spi/v1/FirestoreRpc.java b/google-cloud-firestore/src/main/java/com/google/cloud/firestore/spi/v1/FirestoreRpc.java index c2172fafb..5c8dd4572 100644 --- a/google-cloud-firestore/src/main/java/com/google/cloud/firestore/spi/v1/FirestoreRpc.java +++ b/google-cloud-firestore/src/main/java/com/google/cloud/firestore/spi/v1/FirestoreRpc.java @@ -31,6 +31,8 @@ import com.google.firestore.v1.BeginTransactionResponse; import com.google.firestore.v1.CommitRequest; import com.google.firestore.v1.CommitResponse; +import com.google.firestore.v1.ExecutePipelineRequest; +import com.google.firestore.v1.ExecutePipelineResponse; import com.google.firestore.v1.ListCollectionIdsRequest; import com.google.firestore.v1.ListDocumentsRequest; import com.google.firestore.v1.ListenRequest; @@ -62,6 +64,10 @@ public interface FirestoreRpc extends AutoCloseable, ServiceRpc { /** Runs a query. */ ServerStreamingCallable runQueryCallable(); + /** Executes a pipeline. */ + ServerStreamingCallable + executePipelineCallable(); + /** Runs an aggregation query. */ ServerStreamingCallable runAggregationQueryCallable(); diff --git a/google-cloud-firestore/src/main/java/com/google/cloud/firestore/spi/v1/GrpcFirestoreRpc.java b/google-cloud-firestore/src/main/java/com/google/cloud/firestore/spi/v1/GrpcFirestoreRpc.java index 9668fbeb0..64c0f6901 100644 --- a/google-cloud-firestore/src/main/java/com/google/cloud/firestore/spi/v1/GrpcFirestoreRpc.java +++ b/google-cloud-firestore/src/main/java/com/google/cloud/firestore/spi/v1/GrpcFirestoreRpc.java @@ -50,6 +50,8 @@ import com.google.firestore.v1.CommitRequest; import com.google.firestore.v1.CommitResponse; import com.google.firestore.v1.DatabaseRootName; +import com.google.firestore.v1.ExecutePipelineRequest; +import com.google.firestore.v1.ExecutePipelineResponse; import com.google.firestore.v1.ListCollectionIdsRequest; import com.google.firestore.v1.ListDocumentsRequest; import com.google.firestore.v1.ListenRequest; @@ -236,6 +238,12 @@ public ServerStreamingCallable runQueryCallab return firestoreStub.runQueryCallable(); } + @Override + public ServerStreamingCallable + executePipelineCallable() { + return firestoreStub.executePipelineCallable(); + } + @Override public ServerStreamingCallable runAggregationQueryCallable() { diff --git a/google-cloud-firestore/src/main/java/com/google/cloud/firestore/telemetry/TelemetryConstants.java b/google-cloud-firestore/src/main/java/com/google/cloud/firestore/telemetry/TelemetryConstants.java index d2ab8bf17..f97dbfc71 100644 --- a/google-cloud-firestore/src/main/java/com/google/cloud/firestore/telemetry/TelemetryConstants.java +++ b/google-cloud-firestore/src/main/java/com/google/cloud/firestore/telemetry/TelemetryConstants.java @@ -59,6 +59,9 @@ public interface TelemetryConstants { String METHOD_NAME_PARTITION_QUERY = "PartitionQuery"; String METHOD_NAME_BULK_WRITER_COMMIT = "BulkWriter.Commit"; String METHOD_NAME_RUN_TRANSACTION = "RunTransaction"; + String METHOD_NAME_PIPELINE_EXECUTE = "Pipeline.Execute"; + String METHOD_NAME_EXECUTE_PIPELINE = "ExecutePipeline"; + String METHOD_NAME_EXECUTE_PIPELINE_EXECUTE = "ExecutePipeline.Execute"; // OpenTelemetry built-in metrics constants String FIRESTORE_RESOURCE_TYPE = "firestore_client_raw"; diff --git a/google-cloud-firestore/src/test/java/com/google/cloud/firestore/BulkWriterTest.java b/google-cloud-firestore/src/test/java/com/google/cloud/firestore/BulkWriterTest.java index 811f08a49..645c854b1 100644 --- a/google-cloud-firestore/src/test/java/com/google/cloud/firestore/BulkWriterTest.java +++ b/google-cloud-firestore/src/test/java/com/google/cloud/firestore/BulkWriterTest.java @@ -1280,7 +1280,7 @@ public ScheduledFuture schedule(Runnable command, long delay, TimeUnit unit) } @Test - @Ignore + @Ignore("b/329596806") public void sendsBackoffBatchAfterOtherEnqueuedBatches() throws Exception { ResponseStubber responseStubber = new ResponseStubber() { diff --git a/google-cloud-firestore/src/test/java/com/google/cloud/firestore/RecursiveDeleteTest.java b/google-cloud-firestore/src/test/java/com/google/cloud/firestore/RecursiveDeleteTest.java index f4a27d48e..9d144cb15 100644 --- a/google-cloud-firestore/src/test/java/com/google/cloud/firestore/RecursiveDeleteTest.java +++ b/google-cloud-firestore/src/test/java/com/google/cloud/firestore/RecursiveDeleteTest.java @@ -327,7 +327,7 @@ public void createsRetryQueryAfterStreamExceptionWithLastReceivedDoc() throws Ex } @Test - @Ignore + @Ignore("b/323498886") public void createsSecondQueryWithCorrectStartAfter() throws Exception { // This test checks that the second query is created with the correct startAfter() once the // RecursiveDelete instance is below the MIN_PENDING_OPS threshold to send the next batch. diff --git a/google-cloud-firestore/src/test/java/com/google/cloud/firestore/TestUtil.java b/google-cloud-firestore/src/test/java/com/google/cloud/firestore/TestUtil.java new file mode 100644 index 000000000..7bb671c1d --- /dev/null +++ b/google-cloud-firestore/src/test/java/com/google/cloud/firestore/TestUtil.java @@ -0,0 +1,40 @@ +/* + * Copyright 2024 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.Value; +import java.util.HashMap; +import java.util.Map; + +public final class TestUtil { + public static Map getAggregateSnapshotData(AggregateQuerySnapshot snapshot) { + Map result = new HashMap<>(); + for (Map.Entry entry : snapshot.getData().entrySet()) { + if (entry.getValue().hasIntegerValue()) { + result.put(entry.getKey(), entry.getValue().getIntegerValue()); + } else if (entry.getValue().hasDoubleValue()) { + result.put(entry.getKey(), entry.getValue().getDoubleValue()); + } else if (entry.getValue().hasNullValue()) { + result.put(entry.getKey(), null); + } else { + throw new IllegalArgumentException("AggregateSnapshot has unrecognized value type"); + } + } + + return result; + } +} diff --git a/google-cloud-firestore/src/test/java/com/google/cloud/firestore/WatchTest.java b/google-cloud-firestore/src/test/java/com/google/cloud/firestore/WatchTest.java index 82ac7d944..91f79d97e 100644 --- a/google-cloud-firestore/src/test/java/com/google/cloud/firestore/WatchTest.java +++ b/google-cloud-firestore/src/test/java/com/google/cloud/firestore/WatchTest.java @@ -171,6 +171,7 @@ public void before() { public void after() { Object[] emptyArray = new Object[0]; assertArrayEquals(exceptions.toArray(), emptyArray); + // TODO(b/460778179): Re-enable, this line is flaky // assertArrayEquals(requests.toArray(), emptyArray); assertArrayEquals(documentSnapshots.toArray(), emptyArray); assertArrayEquals(querySnapshots.toArray(), emptyArray); diff --git a/google-cloud-firestore/src/test/java/com/google/cloud/firestore/it/ITBaseTest.java b/google-cloud-firestore/src/test/java/com/google/cloud/firestore/it/ITBaseTest.java index 52ed3dc11..32c8ba852 100644 --- a/google-cloud-firestore/src/test/java/com/google/cloud/firestore/it/ITBaseTest.java +++ b/google-cloud-firestore/src/test/java/com/google/cloud/firestore/it/ITBaseTest.java @@ -51,6 +51,34 @@ public abstract class ITBaseTest { private FirestoreOptions firestoreOptions; private boolean backendPrimed = false; + protected enum FirestoreEdition { + STANDARD, + ENTERPRISE + } + + static String getTargetBackend() { + String targetPropertyName = "FIRESTORE_TARGET_BACKEND"; + String targetBackend = System.getProperty(targetPropertyName); + if (targetBackend == null) { + targetBackend = System.getenv(targetPropertyName); + } + + return targetBackend; + } + + static FirestoreEdition getFirestoreEdition() { + String editionPropertyName = "FIRESTORE_EDITION"; + String firestoreEdition = System.getProperty(editionPropertyName); + if (firestoreEdition == null) { + firestoreEdition = System.getenv(editionPropertyName); + } + + if (firestoreEdition == null) { + return FirestoreEdition.STANDARD; + } + return FirestoreEdition.valueOf(firestoreEdition.toUpperCase()); + } + @Before public void before() throws Exception { FirestoreOptions.Builder optionsBuilder = FirestoreOptions.newBuilder(); @@ -67,11 +95,7 @@ public void before() throws Exception { logger.log(Level.INFO, "Integration test using default database."); } - String targetPropertyName = "FIRESTORE_TARGET_BACKEND"; - String targetBackend = System.getProperty(targetPropertyName); - if (targetBackend == null) { - targetBackend = System.getenv(targetPropertyName); - } + String targetBackend = getTargetBackend(); TransportChannelProvider defaultProvider = optionsBuilder.build().getTransportChannelProvider(); if (targetBackend != null) { if (targetBackend.equals("PROD")) { @@ -82,8 +106,8 @@ public void before() throws Exception { } else if (targetBackend.equals("NIGHTLY")) { optionsBuilder.setChannelProvider( defaultProvider.withEndpoint("test-firestore.sandbox.googleapis.com:443")); - } else { - throw new IllegalArgumentException("Illegal target backend: " + targetBackend); + } else if (targetBackend.equals("EMULATOR")) { + optionsBuilder.setEmulatorHost("localhost:8080"); } } diff --git a/google-cloud-firestore/src/test/java/com/google/cloud/firestore/it/ITBulkWriterTest.java b/google-cloud-firestore/src/test/java/com/google/cloud/firestore/it/ITBulkWriterTest.java index 8e51d798e..931a28735 100644 --- a/google-cloud-firestore/src/test/java/com/google/cloud/firestore/it/ITBulkWriterTest.java +++ b/google-cloud-firestore/src/test/java/com/google/cloud/firestore/it/ITBulkWriterTest.java @@ -83,7 +83,7 @@ public void bulkWriterCreateAddsPrecondition() throws Exception { result.get(); fail("Create operation should have thrown exception"); } catch (Exception e) { - assertTrue(e.getMessage().contains("Document already exists")); + assertTrue(e.getMessage().contains("already exists")); } } @@ -126,7 +126,7 @@ public void bulkWriterUpdateAddsPrecondition() throws Exception { result.get(); fail("Update operation should have thrown exception"); } catch (Exception e) { - assertTrue(e.getMessage().matches(".* No (document|entity) to update.*")); + assertTrue(e.getMessage().contains("to update")); } } diff --git a/google-cloud-firestore/src/test/java/com/google/cloud/firestore/it/ITE2ETracingTest.java b/google-cloud-firestore/src/test/java/com/google/cloud/firestore/it/ITE2ETracingTest.java index b9c05ff2f..8f41c627f 100644 --- a/google-cloud-firestore/src/test/java/com/google/cloud/firestore/it/ITE2ETracingTest.java +++ b/google-cloud-firestore/src/test/java/com/google/cloud/firestore/it/ITE2ETracingTest.java @@ -39,6 +39,7 @@ import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertNull; import static org.junit.Assert.assertTrue; +import static org.junit.Assume.assumeFalse; import static org.junit.Assume.assumeTrue; import com.google.api.gax.rpc.NotFoundException; @@ -339,6 +340,10 @@ public void before() throws Exception { "Error instantiating Firestore. Check that the service account credentials " + "were properly set."); + assumeFalse( + "ITTracingTest is not supported against the emulator.", + "EMULATOR".equals(getTargetBackend())); + // Set up the tracer for custom TraceID injection rootSpanName = String.format("%s%d", this.getClass().getSimpleName(), System.currentTimeMillis()); diff --git a/google-cloud-firestore/src/test/java/com/google/cloud/firestore/it/ITPipelineTest.java b/google-cloud-firestore/src/test/java/com/google/cloud/firestore/it/ITPipelineTest.java new file mode 100644 index 000000000..f829187bd --- /dev/null +++ b/google-cloud-firestore/src/test/java/com/google/cloud/firestore/it/ITPipelineTest.java @@ -0,0 +1,2705 @@ +/* + * Copyright 2024 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.it; + +import static com.google.cloud.firestore.it.ITQueryTest.map; +import static com.google.cloud.firestore.it.TestHelper.isRunningAgainstFirestoreEmulator; +import static com.google.cloud.firestore.pipeline.expressions.AggregateFunction.count; +import static com.google.cloud.firestore.pipeline.expressions.AggregateFunction.countAll; +import static com.google.cloud.firestore.pipeline.expressions.AggregateFunction.countDistinct; +import static com.google.cloud.firestore.pipeline.expressions.AggregateFunction.countIf; +import static com.google.cloud.firestore.pipeline.expressions.AggregateFunction.sum; +import static com.google.cloud.firestore.pipeline.expressions.Expression.add; +import static com.google.cloud.firestore.pipeline.expressions.Expression.and; +import static com.google.cloud.firestore.pipeline.expressions.Expression.array; +import static com.google.cloud.firestore.pipeline.expressions.Expression.arrayContains; +import static com.google.cloud.firestore.pipeline.expressions.Expression.arrayContainsAll; +import static com.google.cloud.firestore.pipeline.expressions.Expression.arrayContainsAny; +import static com.google.cloud.firestore.pipeline.expressions.Expression.arrayGet; +import static com.google.cloud.firestore.pipeline.expressions.Expression.arrayReverse; +import static com.google.cloud.firestore.pipeline.expressions.Expression.ceil; +import static com.google.cloud.firestore.pipeline.expressions.Expression.concat; +import static com.google.cloud.firestore.pipeline.expressions.Expression.conditional; +import static com.google.cloud.firestore.pipeline.expressions.Expression.constant; +import static com.google.cloud.firestore.pipeline.expressions.Expression.cosineDistance; +import static com.google.cloud.firestore.pipeline.expressions.Expression.dotProduct; +import static com.google.cloud.firestore.pipeline.expressions.Expression.endsWith; +import static com.google.cloud.firestore.pipeline.expressions.Expression.equal; +import static com.google.cloud.firestore.pipeline.expressions.Expression.euclideanDistance; +import static com.google.cloud.firestore.pipeline.expressions.Expression.exp; +import static com.google.cloud.firestore.pipeline.expressions.Expression.field; +import static com.google.cloud.firestore.pipeline.expressions.Expression.floor; +import static com.google.cloud.firestore.pipeline.expressions.Expression.greaterThan; +import static com.google.cloud.firestore.pipeline.expressions.Expression.lessThan; +import static com.google.cloud.firestore.pipeline.expressions.Expression.ln; +import static com.google.cloud.firestore.pipeline.expressions.Expression.log; +import static com.google.cloud.firestore.pipeline.expressions.Expression.logicalMaximum; +import static com.google.cloud.firestore.pipeline.expressions.Expression.logicalMinimum; +import static com.google.cloud.firestore.pipeline.expressions.Expression.mapMerge; +import static com.google.cloud.firestore.pipeline.expressions.Expression.mapRemove; +import static com.google.cloud.firestore.pipeline.expressions.Expression.notEqual; +import static com.google.cloud.firestore.pipeline.expressions.Expression.nullValue; +import static com.google.cloud.firestore.pipeline.expressions.Expression.or; +import static com.google.cloud.firestore.pipeline.expressions.Expression.pow; +import static com.google.cloud.firestore.pipeline.expressions.Expression.regexMatch; +import static com.google.cloud.firestore.pipeline.expressions.Expression.round; +import static com.google.cloud.firestore.pipeline.expressions.Expression.sqrt; +import static com.google.cloud.firestore.pipeline.expressions.Expression.startsWith; +import static com.google.cloud.firestore.pipeline.expressions.Expression.stringConcat; +import static com.google.cloud.firestore.pipeline.expressions.Expression.substring; +import static com.google.cloud.firestore.pipeline.expressions.Expression.subtract; +import static com.google.cloud.firestore.pipeline.expressions.Expression.timestampAdd; +import static com.google.cloud.firestore.pipeline.expressions.Expression.timestampToUnixMicros; +import static com.google.cloud.firestore.pipeline.expressions.Expression.timestampToUnixMillis; +import static com.google.cloud.firestore.pipeline.expressions.Expression.timestampToUnixSeconds; +import static com.google.cloud.firestore.pipeline.expressions.Expression.unixMicrosToTimestamp; +import static com.google.cloud.firestore.pipeline.expressions.Expression.unixMillisToTimestamp; +import static com.google.cloud.firestore.pipeline.expressions.Expression.unixSecondsToTimestamp; +import static com.google.cloud.firestore.pipeline.expressions.Expression.vector; +import static com.google.cloud.firestore.pipeline.expressions.Expression.vectorLength; +import static com.google.cloud.firestore.pipeline.expressions.Expression.xor; +import static com.google.common.truth.Truth.assertThat; +import static org.junit.Assert.assertThrows; +import static org.junit.Assume.assumeFalse; + +import com.google.api.gax.rpc.ApiException; +import com.google.api.gax.rpc.StatusCode; +import com.google.cloud.Timestamp; +import com.google.cloud.firestore.Blob; +import com.google.cloud.firestore.CollectionReference; +import com.google.cloud.firestore.FieldValue; +import com.google.cloud.firestore.Firestore; +import com.google.cloud.firestore.FirestoreOptions; +import com.google.cloud.firestore.GeoPoint; +import com.google.cloud.firestore.LocalFirestoreHelper; +import com.google.cloud.firestore.Pipeline; +import com.google.cloud.firestore.PipelineResult; +import com.google.cloud.firestore.pipeline.expressions.AggregateFunction; +import com.google.cloud.firestore.pipeline.expressions.Expression; +import com.google.cloud.firestore.pipeline.expressions.Field; +import com.google.cloud.firestore.pipeline.stages.Aggregate; +import com.google.cloud.firestore.pipeline.stages.AggregateHints; +import com.google.cloud.firestore.pipeline.stages.AggregateOptions; +import com.google.cloud.firestore.pipeline.stages.CollectionHints; +import com.google.cloud.firestore.pipeline.stages.CollectionOptions; +import com.google.cloud.firestore.pipeline.stages.ExplainOptions; +import com.google.cloud.firestore.pipeline.stages.FindNearest; +import com.google.cloud.firestore.pipeline.stages.FindNearestOptions; +import com.google.cloud.firestore.pipeline.stages.PipelineExecuteOptions; +import com.google.cloud.firestore.pipeline.stages.RawOptions; +import com.google.cloud.firestore.pipeline.stages.RawStage; +import com.google.cloud.firestore.pipeline.stages.Sample; +import com.google.cloud.firestore.pipeline.stages.UnnestOptions; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableMap; +import com.google.common.collect.Lists; +import java.util.Date; +import java.util.List; +import java.util.Map; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; +import java.util.stream.Collectors; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + +@RunWith(JUnit4.class) +public class ITPipelineTest extends ITBaseTest { + private CollectionReference collection; + private Map> bookDocs; + + public CollectionReference testCollectionWithDocs(Map> docs) + throws ExecutionException, InterruptedException, TimeoutException { + CollectionReference collection = firestore.collection(LocalFirestoreHelper.autoId()); + for (Map.Entry> doc : docs.entrySet()) { + collection.document(doc.getKey()).set(doc.getValue()).get(5, TimeUnit.SECONDS); + } + return collection; + } + + List> data(List results) { + return results.stream().map(PipelineResult::getData).collect(Collectors.toList()); + } + + @Before + public void setup() throws Exception { + assumeFalse( + "This test suite only runs against the Enterprise edition.", + !getFirestoreEdition().equals(FirestoreEdition.ENTERPRISE)); + if (collection != null) { + return; + } + + bookDocs = + ImmutableMap.>builder() + .put( + "book1", + ImmutableMap.builder() + .put("title", "The Hitchhiker's Guide to the Galaxy") + .put("author", "Douglas Adams") + .put("genre", "Science Fiction") + .put("published", 1979) + .put("rating", 4.2) + .put("tags", ImmutableList.of("comedy", "space", "adventure")) + .put("awards", ImmutableMap.of("hugo", true, "nebula", false)) + .put( + "embedding", + FieldValue.vector( + new double[] {10.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0})) + .build()) + .put( + "book2", + ImmutableMap.builder() + .put("title", "Pride and Prejudice") + .put("author", "Jane Austen") + .put("genre", "Romance") + .put("published", 1813) + .put("rating", 4.5) + .put("tags", ImmutableList.of("classic", "social commentary", "love")) + .put("awards", ImmutableMap.of("none", true)) + .put( + "embedding", + FieldValue.vector( + new double[] {1.0, 10.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0})) + .build()) + .put( + "book3", + ImmutableMap.builder() + .put("title", "One Hundred Years of Solitude") + .put("author", "Gabriel García Márquez") + .put("genre", "Magical Realism") + .put("published", 1967) + .put("rating", 4.3) + .put("tags", ImmutableList.of("family", "history", "fantasy")) + .put("awards", ImmutableMap.of("nobel", true, "nebula", false)) + .put( + "embedding", + FieldValue.vector( + new double[] {1.0, 1.0, 10.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0})) + .build()) + .put( + "book4", + ImmutableMap.builder() + .put("title", "The Lord of the Rings") + .put("author", "J.R.R. Tolkien") + .put("genre", "Fantasy") + .put("published", 1954) + .put("rating", 4.7) + .put("tags", ImmutableList.of("adventure", "magic", "epic")) + .put("awards", ImmutableMap.of("hugo", false, "nebula", false)) + .put("cost", Double.NaN) + .put( + "embedding", + FieldValue.vector( + new double[] {1.0, 1.0, 1.0, 10.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0})) + .build()) + .put( + "book5", + ImmutableMap.builder() + .put("title", "The Handmaid's Tale") + .put("author", "Margaret Atwood") + .put("genre", "Dystopian") + .put("published", 1985) + .put("rating", 4.1) + .put("tags", ImmutableList.of("feminism", "totalitarianism", "resistance")) + .put("awards", ImmutableMap.of("arthur c. clarke", true, "booker prize", false)) + .put( + "embedding", + FieldValue.vector( + new double[] {1.0, 1.0, 1.0, 1.0, 10.0, 1.0, 1.0, 1.0, 1.0, 1.0})) + .build()) + .put( + "book6", + ImmutableMap.builder() + .put("title", "Crime and Punishment") + .put("author", "Fyodor Dostoevsky") + .put("genre", "Psychological Thriller") + .put("published", 1866) + .put("rating", 4.3) + .put("tags", ImmutableList.of("philosophy", "crime", "redemption")) + .put("awards", ImmutableMap.of("none", true)) + .put( + "embedding", + FieldValue.vector( + new double[] {1.0, 1.0, 1.0, 1.0, 1.0, 10.0, 1.0, 1.0, 1.0, 1.0})) + .build()) + .put( + "book7", + ImmutableMap.builder() + .put("title", "To Kill a Mockingbird") + .put("author", "Harper Lee") + .put("genre", "Southern Gothic") + .put("published", 1960) + .put("rating", 4.2) + .put("tags", ImmutableList.of("racism", "injustice", "coming-of-age")) + .put("awards", ImmutableMap.of("pulitzer", true)) + .put( + "embedding", + FieldValue.vector( + new double[] {1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 10.0, 1.0, 1.0, 1.0})) + .build()) + .put( + "book8", + ImmutableMap.builder() + .put("title", "1984") + .put("author", "George Orwell") + .put("genre", "Dystopian") + .put("published", 1949) + .put("rating", 4.2) + .put("tags", ImmutableList.of("surveillance", "totalitarianism", "propaganda")) + .put("awards", ImmutableMap.of("prometheus", true)) + .put( + "embedding", + FieldValue.vector( + new double[] {1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 10.0, 1.0, 1.0})) + .build()) + .put( + "book9", + ImmutableMap.builder() + .put("title", "The Great Gatsby") + .put("author", "F. Scott Fitzgerald") + .put("genre", "Modernist") + .put("published", 1925) + .put("rating", 4.0) + .put("tags", ImmutableList.of("wealth", "american dream", "love")) + .put("awards", ImmutableMap.of("none", true)) + .put( + "embedding", + FieldValue.vector( + new double[] {1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 10.0, 1.0})) + .build()) + .put( + "book10", + ImmutableMap.builder() + .put("title", "Dune") + .put("author", "Frank Herbert") + .put("genre", "Science Fiction") + .put("published", 1965) + .put("rating", 4.6) + .put("tags", ImmutableList.of("politics", "desert", "ecology")) + .put("awards", ImmutableMap.of("hugo", true, "nebula", true)) + .put( + "embedding", + FieldValue.vector( + new double[] {1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 10.0})) + .build()) + .put( + "book11", + ImmutableMap.builder() + .put("title", "Timestamp Book") + .put("author", "Timestamp Author") + .put("timestamp", new Date()) + .build()) + .build(); + collection = testCollectionWithDocs(bookDocs); + } + + @Test + public void testAllDataTypes() throws Exception { + Date refDate = new Date(); + Timestamp refTimestamp = Timestamp.now(); + GeoPoint refGeoPoint = new GeoPoint(1, 2); + byte[] refBytes = new byte[] {1, 2, 3}; + double[] refVector = new double[] {1.0, 2.0, 3.0}; + + Map refMap = + map( + "number", + 1, + "string", + "a string", + "boolean", + true, + "null", + null, + "geoPoint", + refGeoPoint, + "timestamp", + refTimestamp, + "date", + Timestamp.of(refDate), + "bytes", + com.google.cloud.firestore.Blob.fromBytes(refBytes), + "vector", + FieldValue.vector(refVector)); + + List refArray = + Lists.newArrayList( + 1, + "a string", + true, + null, + refTimestamp, + refGeoPoint, + Timestamp.of(refDate), + com.google.cloud.firestore.Blob.fromBytes(refBytes), + FieldValue.vector(refVector)); + + Pipeline pipeline = + firestore + .pipeline() + .collection(collection.getPath()) + .limit(1) + .select( + constant(1L).as("number"), + constant("a string").as("string"), + constant(true).as("boolean"), + nullValue().as("null"), + constant(refTimestamp).as("timestamp"), + constant(refDate).as("date"), + constant(refGeoPoint).as("geoPoint"), + constant(com.google.cloud.firestore.Blob.fromBytes(refBytes)).as("bytes"), + vector(refVector).as("vector"), + Expression.map(refMap).as("map"), + array(refArray).as("array")); + + List results = pipeline.execute().get().getResults(); + assertThat(results).hasSize(1); + Map data = results.get(0).getData(); + + assertThat(data.get("number")).isEqualTo(1L); + assertThat(data.get("string")).isEqualTo("a string"); + assertThat(data.get("boolean")).isEqualTo(true); + assertThat(data.get("null")).isNull(); + assertThat(data.get("geoPoint")).isEqualTo(refGeoPoint); + assertThat(data.get("timestamp")).isEqualTo(refTimestamp); + assertThat(data.get("date")).isEqualTo(Timestamp.of(refDate)); + assertThat(data.get("bytes")).isEqualTo(com.google.cloud.firestore.Blob.fromBytes(refBytes)); + assertThat(data.get("vector")).isEqualTo(FieldValue.vector(refVector)); + assertThat(stringOfOrderedKeyValues((Map) data.get("map"))) + .isEqualTo(stringOfOrderedKeyValues(refMap)); + assertThat(data.get("array").toString()).isEqualTo(refArray.toString()); + } + + private String stringOfOrderedKeyValues(Map map) { + return map.entrySet().stream() + .sorted(Map.Entry.comparingByKey()) + .map(e -> e.getKey() + "=" + e.getValue()) + .collect(Collectors.joining(", ")); + } + + @Test + public void testResultMetadata() throws Exception { + Pipeline pipeline = firestore.pipeline().collection(collection.getPath()); + Pipeline.Snapshot snapshot = pipeline.execute().get(); + assertThat(snapshot.getExecutionTime()).isNotNull(); + + for (PipelineResult result : snapshot.getResults()) { + assertThat(result.getCreateTime()).isAtMost(result.getUpdateTime()); + assertThat(result.getUpdateTime()).isLessThan(result.getExecutionTime()); + } + + collection.document("book1").update("rating", 5.0).get(); + snapshot = + pipeline.where(equal("title", "The Hitchhiker's Guide to the Galaxy")).execute().get(); + for (PipelineResult result : snapshot.getResults()) { + assertThat(result.getCreateTime()).isLessThan(result.getUpdateTime()); + } + } + + @Test + public void testResultIsEqual() throws Exception { + Pipeline pipeline = + firestore.pipeline().collection(collection.getPath()).sort(field("title").ascending()); + Pipeline.Snapshot snapshot1 = pipeline.limit(1).execute().get(); + Pipeline.Snapshot snapshot2 = pipeline.limit(1).execute().get(); + Pipeline.Snapshot snapshot3 = pipeline.offset(1).limit(1).execute().get(); + + assertThat(snapshot1.getResults()).hasSize(1); + assertThat(snapshot2.getResults()).hasSize(1); + assertThat(snapshot3.getResults()).hasSize(1); + assertThat(snapshot1.getResults().get(0)).isEqualTo(snapshot2.getResults().get(0)); + assertThat(snapshot1.getResults().get(0)).isNotEqualTo(snapshot3.getResults().get(0)); + } + + @Test + public void testEmptyResultMetadata() throws Exception { + Pipeline pipeline = firestore.pipeline().collection(collection.getPath()).limit(0); + Pipeline.Snapshot snapshot = pipeline.execute().get(); + assertThat(snapshot.getResults()).isEmpty(); + assertThat(snapshot.getExecutionTime()).isNotNull(); + // Ensure execution time is recent, within a tolerance. + long now = Timestamp.now().toDate().getTime(); + long executionTime = snapshot.getExecutionTime().toDate().getTime(); + assertThat(now - executionTime).isLessThan(3000); // 3 seconds tolerance + } + + @Test + public void testAggregateResultMetadata() throws Exception { + Pipeline pipeline = + firestore.pipeline().collection(collection.getPath()).aggregate(countAll().as("count")); + Pipeline.Snapshot snapshot = pipeline.execute().get(); + assertThat(snapshot.getResults()).hasSize(1); + assertThat(snapshot.getExecutionTime()).isNotNull(); + + PipelineResult aggregateResult = snapshot.getResults().get(0); + assertThat(aggregateResult.getCreateTime()).isNull(); + assertThat(aggregateResult.getUpdateTime()).isNull(); + + // Ensure execution time is recent, within a tolerance. + long now = Timestamp.now().toDate().getTime(); + long executionTime = snapshot.getExecutionTime().toDate().getTime(); + assertThat(now - executionTime).isLessThan(3000); // 3 seconds tolerance + } + + @Test + public void testAggregates() throws Exception { + List results = + firestore + .pipeline() + .collection(collection.getPath()) + .aggregate(countAll().as("count")) + .execute() + .get() + .getResults(); + assertThat(data(results)).isEqualTo(Lists.newArrayList(map("count", 11L))); + + results = + firestore + .pipeline() + .createFrom(collection) + .where(equal("genre", "Science Fiction")) + .aggregate( + countAll().as("count"), + AggregateFunction.average("rating").as("avg_rating"), + field("rating").maximum().as("max_rating")) + .execute() + .get() + .getResults(); + assertThat(data(results)) + .isEqualTo(Lists.newArrayList(map("count", 2L, "avg_rating", 4.4, "max_rating", 4.6))); + } + + @Test + public void testMoreAggregates() throws Exception { + List results = + firestore + .pipeline() + .createFrom(collection) + .aggregate( + sum("rating").as("sum_rating"), + count("rating").as("count_rating"), + countDistinct("genre").as("distinct_genres")) + .execute() + .get() + .getResults(); + Map result = data(results).get(0); + assertThat((Double) result.get("sum_rating")).isWithin(0.00001).of(43.1); + assertThat(result.get("count_rating")).isEqualTo(10L); + assertThat(result.get("distinct_genres")).isEqualTo(8L); + } + + @Test + public void testCountIfAggregate() throws Exception { + List results = + firestore + .pipeline() + .createFrom(collection) + .aggregate(countIf(Expression.greaterThan(field("rating"), 4.3)).as("count")) + .execute() + .get() + .getResults(); + assertThat(data(results)).isEqualTo(Lists.newArrayList(map("count", 3L))); + } + + @Test + public void testGroupBysWithoutAccumulators() throws Exception { + assertThrows( + IllegalArgumentException.class, + () -> { + firestore + .pipeline() + .createFrom(collection) + .where(lessThan("published", 1900)) + .aggregate(Aggregate.withAccumulators().withGroups("genre")); + }); + } + + @Test + public void testDistinct() throws Exception { + List results = + firestore + .pipeline() + .createFrom(collection) + .where(lessThan("published", 1900)) + .distinct(field("genre").toLower().as("lower_genre")) + .execute() + .get() + .getResults(); + assertThat(data(results)) + .containsExactly( + map("lower_genre", "romance"), map("lower_genre", "psychological thriller")); + } + + @Test + public void testGroupBysAndAggregate() throws Exception { + List results = + firestore + .pipeline() + .createFrom(collection) + .where(lessThan("published", 1984)) + .aggregate( + Aggregate.withAccumulators(AggregateFunction.average("rating").as("avg_rating")) + .withGroups("genre")) + .where(greaterThan("avg_rating", 4.3)) + .execute() + .get() + .getResults(); + assertThat(data(results)) + .containsExactly( + map("avg_rating", 4.7, "genre", "Fantasy"), + map("avg_rating", 4.5, "genre", "Romance"), + map("avg_rating", 4.4, "genre", "Science Fiction")); + } + + @Test + public void testMinMax() throws Exception { + List results = + firestore + .pipeline() + .createFrom(collection) + .aggregate( + countAll().as("count"), + field("rating").maximum().as("max_rating"), + field("published").minimum().as("min_published")) + .execute() + .get() + .getResults(); + assertThat(data(results)) + .isEqualTo( + Lists.newArrayList( + map( + "count", 11L, + "max_rating", 4.7, + "min_published", 1813L))); + } + + @Test + public void selectSpecificFields() throws Exception { + List results = + firestore + .pipeline() + .collection(collection.getPath()) + .select("title", "author") + .sort(field("author").ascending()) + .execute() + .get() + .getResults(); + + assertThat(data(results)) + .isEqualTo( + Lists.newArrayList( + map("title", "The Hitchhiker's Guide to the Galaxy", "author", "Douglas Adams"), + map("title", "The Great Gatsby", "author", "F. Scott Fitzgerald"), + map("title", "Dune", "author", "Frank Herbert"), + map("title", "Crime and Punishment", "author", "Fyodor Dostoevsky"), + map("title", "One Hundred Years of Solitude", "author", "Gabriel García Márquez"), + map("title", "1984", "author", "George Orwell"), + map("title", "To Kill a Mockingbird", "author", "Harper Lee"), + map("title", "The Lord of the Rings", "author", "J.R.R. Tolkien"), + map("title", "Pride and Prejudice", "author", "Jane Austen"), + map("title", "The Handmaid's Tale", "author", "Margaret Atwood"), + map("title", "Timestamp Book", "author", "Timestamp Author"))); + } + + @Test + public void addAndRemoveFields() throws Exception { + List results = + firestore + .pipeline() + .collection(collection.getPath()) + .where(field("author").notEqual("Timestamp Author")) + .addFields( + Expression.stringConcat(field("author"), "_", field("title")).as("author_title"), + Expression.stringConcat(field("title"), "_", field("author")).as("title_author")) + .removeFields("title_author", "tags", "awards", "rating", "title", "embedding", "cost") + .removeFields(field("published"), field("genre"), field("nestedField")) + .sort(field("author_title").ascending()) + .execute() + .get() + .getResults(); + + assertThat(data(results)) + .isEqualTo( + Lists.newArrayList( + map( + "author_title", + "Douglas Adams_The Hitchhiker's Guide to the Galaxy", + "author", + "Douglas Adams"), + map( + "author_title", + "F. Scott Fitzgerald_The Great Gatsby", + "author", + "F. Scott Fitzgerald"), + map("author_title", "Frank Herbert_Dune", "author", "Frank Herbert"), + map( + "author_title", + "Fyodor Dostoevsky_Crime and Punishment", + "author", + "Fyodor Dostoevsky"), + map( + "author_title", + "Gabriel García Márquez_One Hundred Years of Solitude", + "author", + "Gabriel García Márquez"), + map("author_title", "George Orwell_1984", "author", "George Orwell"), + map("author_title", "Harper Lee_To Kill a Mockingbird", "author", "Harper Lee"), + map( + "author_title", + "J.R.R. Tolkien_The Lord of the Rings", + "author", + "J.R.R. Tolkien"), + map("author_title", "Jane Austen_Pride and Prejudice", "author", "Jane Austen"), + map( + "author_title", + "Margaret Atwood_The Handmaid's Tale", + "author", + "Margaret Atwood"))); + } + + @Test + public void whereByMultipleConditions() throws Exception { + List results = + firestore + .pipeline() + .createFrom(collection) + .where(and(greaterThan("rating", 4.5), equal("genre", "Science Fiction"))) + .execute() + .get() + .getResults(); + + // It's Dune + assertThat(data(results)) + .isEqualTo(Lists.newArrayList(collection.document("book10").get().get().getData())); + assertThat(results.get(0).getReference()).isEqualTo(collection.document("book10")); + } + + @Test + public void whereByOrCondition() throws Exception { + List results = + firestore + .pipeline() + .createFrom(collection) + .where(or(equal("genre", "Romance"), equal("genre", "Dystopian"))) + .select("title") + .execute() + .get() + .getResults(); + + assertThat(data(results)) + .isEqualTo( + Lists.newArrayList( + map("title", "Pride and Prejudice"), + map("title", "The Handmaid's Tale"), + map("title", "1984"))); + } + + @Test + public void testPipelineWithOffsetAndLimit() throws Exception { + List results = + firestore + .pipeline() + .collection(collection.getPath()) + .sort(field("author").ascending()) + .offset(5) + .limit(3) + .select("title", "author") + .execute() + .get() + .getResults(); + + assertThat(data(results)) + .isEqualTo( + Lists.newArrayList( + map("title", "1984", "author", "George Orwell"), + map("title", "To Kill a Mockingbird", "author", "Harper Lee"), + map("title", "The Lord of the Rings", "author", "J.R.R. Tolkien"))); + } + + @Test + public void testArrayContains() throws Exception { + List results = + firestore + .pipeline() + .createFrom(collection) + .where(arrayContains("tags", "comedy")) + .execute() + .get() + .getResults(); + assertThat(data(results)) + // The Hitchhiker's Guide to the Galaxy + .isEqualTo(Lists.newArrayList(collection.document("book1").get().get().getData())); + } + + @Test + public void testArrayContainsAny() throws Exception { + List results = + firestore + .pipeline() + .createFrom(collection) + .where(arrayContainsAny("tags", Lists.newArrayList("comedy", "classic"))) + .select("title") + .execute() + .get() + .getResults(); + + assertThat(data(results)) + .isEqualTo( + Lists.newArrayList( + map("title", "The Hitchhiker's Guide to the Galaxy"), + map("title", "Pride and Prejudice"))); + } + + @Test + public void testArrayContainsAll() throws Exception { + List results = + firestore + .pipeline() + .createFrom(collection) + .where(arrayContainsAll("tags", Lists.newArrayList("adventure", "magic"))) + .select("title") + .execute() + .get() + .getResults(); + + assertThat(data(results)).isEqualTo(Lists.newArrayList(map("title", "The Lord of the Rings"))); + } + + @Test + public void testArrayLength() throws Exception { + List results = + firestore + .pipeline() + .createFrom(collection) + .select(field("tags").arrayLength().as("tagsCount")) + .where(equal("tagsCount", 3)) + .execute() + .get() + .getResults(); + + // All documents have 3 tags in the test dataset + assertThat(data(results)).hasSize(10); + } + + @Test + public void testArrayConcat() throws Exception { + List results = + firestore + .pipeline() + .createFrom(collection) + .select(field("tags").arrayConcat(array("newTag1", "newTag2")).as("modifiedTags")) + .limit(1) + .execute() + .get() + .getResults(); + + assertThat(data(results)) + .isEqualTo( + Lists.newArrayList( + map( + "modifiedTags", + Lists.newArrayList("comedy", "space", "adventure", "newTag1", "newTag2")))); + } + + @Test + public void testStrConcat() throws Exception { + List results = + firestore + .pipeline() + .createFrom(collection) + .select(Expression.stringConcat(field("author"), " - ", field("title")).as("bookInfo")) + .limit(1) + .execute() + .get() + .getResults(); + + assertThat(data(results)) + .isEqualTo( + Lists.newArrayList( + map("bookInfo", "Douglas Adams - The Hitchhiker's Guide to the Galaxy"))); + } + + @Test + public void testStartsWith() throws Exception { + List results = + firestore + .pipeline() + .createFrom(collection) + .where(startsWith("title", "The")) + .select("title") + .sort(field("title").ascending()) + .execute() + .get() + .getResults(); + + assertThat(data(results)) + .isEqualTo( + Lists.newArrayList( + map("title", "The Great Gatsby"), + map("title", "The Handmaid's Tale"), + map("title", "The Hitchhiker's Guide to the Galaxy"), + map("title", "The Lord of the Rings"))); + } + + @Test + public void testEndsWith() throws Exception { + List results = + firestore + .pipeline() + .createFrom(collection) + .where(endsWith(field("title"), constant("y"))) + .select("title") + .sort(field("title").descending()) + .execute() + .get() + .getResults(); + + assertThat(data(results)) + .isEqualTo( + Lists.newArrayList( + map("title", "The Hitchhiker's Guide to the Galaxy"), + map("title", "The Great Gatsby"))); + } + + @Test + public void testLength() throws Exception { + List results = + firestore + .pipeline() + .createFrom(collection) + .select(field("title").charLength().as("titleLength"), field("title")) + .where(greaterThan("titleLength", 21)) + .sort(field("titleLength").descending()) + .execute() + .get() + .getResults(); + + assertThat(data(results)) + .isEqualTo( + Lists.newArrayList( + map("titleLength", 36L, "title", "The Hitchhiker's Guide to the Galaxy"), + map("titleLength", 29L, "title", "One Hundred Years of Solitude"))); + } + + @Test + public void testStringFunctions() throws Exception { + List results; + + // Reverse + results = + firestore + .pipeline() + .collection(collection.getPath()) + .select(field("title").reverse().as("reversed_title"), field("author")) + .where(field("author").equal("Douglas Adams")) + .execute() + .get() + .getResults(); + assertThat(data(results).get(0).get("reversed_title")) + .isEqualTo("yxalaG eht ot ediuG s'rekihhctiH ehT"); + + // CharLength + results = + firestore + .pipeline() + .createFrom(collection) + .select(field("title").charLength().as("title_length"), field("author")) + .where(field("author").equal("Douglas Adams")) + .execute() + .get() + .getResults(); + assertThat(data(results).get(0).get("title_length")).isEqualTo(36L); + + // ByteLength + results = + firestore + .pipeline() + .createFrom(collection) + .select( + field("author"), + field("title").stringConcat("_银河系漫游指南").byteLength().as("title_byte_length")) + .where(field("author").equal("Douglas Adams")) + .execute() + .get() + .getResults(); + assertThat(data(results).get(0).get("title_byte_length")).isEqualTo(58L); + } + + @Test + public void testToLowercase() throws Exception { + List results = + firestore + .pipeline() + .createFrom(collection) + .select(field("title").toLower().as("lowercaseTitle")) + .limit(1) + .execute() + .get() + .getResults(); + + assertThat(data(results)) + .isEqualTo( + Lists.newArrayList(map("lowercaseTitle", "the hitchhiker's guide to the galaxy"))); + } + + @Test + public void testToUppercase() throws Exception { + List results = + firestore + .pipeline() + .createFrom(collection) + .select(field("author").toUpper().as("uppercaseAuthor")) + .limit(1) + .execute() + .get() + .getResults(); + + assertThat(data(results)) + .isEqualTo(Lists.newArrayList(map("uppercaseAuthor", "DOUGLAS ADAMS"))); + } + + @Test + public void testTrim() throws Exception { + List results = + firestore + .pipeline() + .createFrom(collection) + .addFields(stringConcat(constant(" "), field("title"), constant(" ")).as("spacedTitle")) + .select(field("spacedTitle").trim().as("trimmedTitle"), field("spacedTitle")) + .limit(1) + .execute() + .get() + .getResults(); + + assertThat(data(results)) + .isEqualTo( + Lists.newArrayList( + map( + "spacedTitle", " The Hitchhiker's Guide to the Galaxy ", + "trimmedTitle", "The Hitchhiker's Guide to the Galaxy"))); + } + + @Test + public void testTrimWithCharacters() throws Exception { + List results = + firestore + .pipeline() + .createFrom(collection) + .addFields(concat(constant("_-"), field("title"), constant("-_")).as("paddedTitle")) + .select(field("paddedTitle").trimValue("_-").as("trimmedTitle"), field("paddedTitle")) + .limit(1) + .execute() + .get() + .getResults(); + + assertThat(data(results)) + .isEqualTo( + Lists.newArrayList( + map( + "paddedTitle", + "_-The Hitchhiker's Guide to the Galaxy-_", + "trimmedTitle", + "The Hitchhiker's Guide to the Galaxy"))); + } + + @Test + public void testLike() throws Exception { + assumeFalse( + "LIKE is not supported against the emulator.", + isRunningAgainstFirestoreEmulator(firestore)); + + List results = + firestore + .pipeline() + .createFrom(collection) + .where(field("title").like("%Guide%")) + .select("title") + .execute() + .get() + .getResults(); + + assertThat(data(results)) + .isEqualTo(Lists.newArrayList(map("title", "The Hitchhiker's Guide to the Galaxy"))); + } + + @Test + public void testRegexContains() throws Exception { + assumeFalse( + "LIKE is not supported against the emulator.", + isRunningAgainstFirestoreEmulator(firestore)); + // Find titles that contain either "the" or "of" (case-insensitive) + List results = + firestore + .pipeline() + .createFrom(collection) + .where(field("title").regexContains("(?i)(the|of)")) + .execute() + .get() + .getResults(); + + assertThat(data(results)).hasSize(5); + } + + @Test + public void testRegexMatches() throws Exception { + assumeFalse( + "LIKE is not supported against the emulator.", + isRunningAgainstFirestoreEmulator(firestore)); + // Find titles that contain either "the" or "of" (case-insensitive) + List results = + firestore + .pipeline() + .createFrom(collection) + .where(regexMatch("title", ".*(?i)(the|of).*")) + .execute() + .get() + .getResults(); + + assertThat(data(results)).hasSize(5); + } + + @Test + public void testArithmeticOperations() throws Exception { + List results = + firestore + .pipeline() + .createFrom(collection) + .select( + add(field("rating"), 1).as("ratingPlusOne"), + subtract(field("published"), 1900).as("yearsSince1900"), + field("rating").multiply(10).as("ratingTimesTen"), + field("rating").divide(2).as("ratingDividedByTwo")) + .limit(1) + .execute() + .get() + .getResults(); + + assertThat(data(results)) + .isEqualTo( + Lists.newArrayList( + map( + "ratingPlusOne", + 5.2, + "yearsSince1900", + 79L, + "ratingTimesTen", + 42.0, + "ratingDividedByTwo", + 2.1))); + } + + @Test + public void testComparisonOperators() throws Exception { + List results = + firestore + .pipeline() + .createFrom(collection) + .where( + and( + greaterThan("rating", 4.2), + field("rating").lessThanOrEqual(4.5), + notEqual("genre", "Science Fiction"))) + .select("rating", "title") + .sort(field("title").ascending()) + .execute() + .get() + .getResults(); + + assertThat(data(results)) + .isEqualTo( + Lists.newArrayList( + map("rating", 4.3, "title", "Crime and Punishment"), + map("rating", 4.3, "title", "One Hundred Years of Solitude"), + map("rating", 4.5, "title", "Pride and Prejudice"))); + } + + @Test + public void testLogicalAndComparisonOperators() throws Exception { + List results = + firestore + .pipeline() + .createFrom(collection) + .where( + xor( + equal("genre", "Romance"), + equal("genre", "Dystopian"), + equal("genre", "Fantasy"), + equal("published", 1949))) + .select("title") + .execute() + .get() + .getResults(); + assertThat(data(results)) + .containsExactly( + map("title", "Pride and Prejudice"), + map("title", "The Lord of the Rings"), + map("title", "The Handmaid's Tale")); + + results = + firestore + .pipeline() + .createFrom(collection) + .where(Expression.equalAny("genre", Lists.newArrayList("Romance", "Dystopian"))) + .select("title") + .execute() + .get() + .getResults(); + assertThat(data(results)) + .containsExactly( + map("title", "Pride and Prejudice"), + map("title", "The Handmaid's Tale"), + map("title", "1984")); + + results = + firestore + .pipeline() + .createFrom(collection) + .where(Expression.notEqualAny("genre", Lists.newArrayList("Romance", "Dystopian"))) + .select("genre") + .distinct("genre") + .execute() + .get() + .getResults(); + assertThat(data(results)) + .containsExactly( + map("genre", "Science Fiction"), + map("genre", "Magical Realism"), + map("genre", "Fantasy"), + map("genre", "Psychological Thriller"), + map("genre", "Southern Gothic"), + map("genre", "Modernist")); + } + + @Test + public void testCondExpression() throws Exception { + List results = + firestore + .pipeline() + .createFrom(collection) + .where(field("title").notEqual("Timestamp Book")) + .select( + conditional(Expression.greaterThan(field("published"), 1980), "Modern", "Classic") + .as("era"), + field("title"), + field("published")) + .sort(field("published").ascending()) + .limit(2) + .execute() + .get() + .getResults(); + assertThat(data(results)) + .isEqualTo( + Lists.newArrayList( + map("era", "Classic", "title", "Pride and Prejudice", "published", 1813L), + map("era", "Classic", "title", "Crime and Punishment", "published", 1866L))); + } + + @Test + public void testLogicalOperators() throws Exception { + List results = + firestore + .pipeline() + .createFrom(collection) + .where( + or( + and(greaterThan("rating", 4.5), equal("genre", "Science Fiction")), + lessThan("published", 1900))) + .select("title") + .sort(field("title").ascending()) + .execute() + .get() + .getResults(); + + assertThat(data(results)) + .isEqualTo( + Lists.newArrayList( + map("title", "Crime and Punishment"), + map("title", "Dune"), + map("title", "Pride and Prejudice"))); + } + + @Test + public void testChecks() throws Exception { + List results = + firestore + .pipeline() + .collection(collection.getPath()) + .sort(field("rating").descending()) + .limit(1) + .select( + field("rating").isNull().as("ratingIsNull"), + field("rating").isNaN().as("ratingIsNaN"), + arrayGet("title", 0).isError().as("isError"), + arrayGet("title", 0) + .ifError(constant("was error"), constant("was not error")) + .as("ifError"), + field("foo").isAbsent().as("isAbsent"), + field("title").isNotNull().as("titleIsNotNull"), + field("cost").isNotNaN().as("costIsNotNan"), + field("fooBarBaz").exists().as("fooBarBazExists"), + field("title").exists().as("titleExists")) + .execute() + .get() + .getResults(); + + assertThat(data(results)) + .isEqualTo( + Lists.newArrayList( + map( + "ratingIsNull", + false, + "ratingIsNaN", + false, + "isError", + false, + "ifError", + "was not error", + "isAbsent", + true, + "titleIsNotNull", + true, + "costIsNotNan", + false, + "fooBarBazExists", + false, + "titleExists", + true))); + } + + @Test + public void testLogicalMinMax() throws Exception { + List results; + + // logicalMax + results = + firestore + .pipeline() + .createFrom(collection) + .where(field("author").equal("Douglas Adams")) + .select( + field("rating").logicalMaximum(4.5).as("max_rating"), + logicalMaximum(field("published"), 1900).as("max_published")) + .execute() + .get() + .getResults(); + assertThat(data(results)).containsExactly(map("max_rating", 4.5, "max_published", 1979L)); + + // logicalMin + results = + firestore + .pipeline() + .createFrom(collection) + .where(field("author").equal("Douglas Adams")) + .select( + field("rating").logicalMinimum(4.5).as("min_rating"), + logicalMinimum(field("published"), 1900).as("min_published")) + .execute() + .get() + .getResults(); + assertThat(data(results)).containsExactly(map("min_rating", 4.2, "min_published", 1900L)); + } + + @Test + public void testMapGet() throws Exception { + List results = + firestore + .pipeline() + .createFrom(collection) + .select(field("awards").mapGet("hugo").as("hugoAward"), field("title")) + .where(equal("hugoAward", true)) + .execute() + .get() + .getResults(); + + assertThat(data(results)) + .isEqualTo( + Lists.newArrayList( + map("hugoAward", true, "title", "The Hitchhiker's Guide to the Galaxy"), + map("hugoAward", true, "title", "Dune"))); + } + + @Test + public void testDataManipulationExpressions() throws Exception { + List results = + firestore + .pipeline() + .createFrom(collection) + .where(equal("title", "Timestamp Book")) + .select( + timestampAdd(field("timestamp"), "day", 1).as("timestamp_plus_day"), + Expression.timestampSubtract(field("timestamp"), "hour", 1) + .as("timestamp_minus_hour")) + .execute() + .get() + .getResults(); + assertThat(results).hasSize(1); + Date originalTimestamp = (Date) bookDocs.get("book11").get("timestamp"); + Timestamp timestampPlusDay = (Timestamp) results.get(0).getData().get("timestamp_plus_day"); + Timestamp timestampMinusHour = (Timestamp) results.get(0).getData().get("timestamp_minus_hour"); + assertThat(timestampPlusDay.toDate().getTime() - originalTimestamp.getTime()) + .isEqualTo(24 * 60 * 60 * 1000); + assertThat(originalTimestamp.getTime() - timestampMinusHour.toDate().getTime()) + .isEqualTo(60 * 60 * 1000); + + results = + firestore + .pipeline() + .createFrom(collection) + .where(equal("title", "The Hitchhiker's Guide to the Galaxy")) + .select( + arrayGet("tags", 1).as("second_tag"), + mapMerge(field("awards"), Expression.map(map("new_award", true))) + .as("merged_awards")) + .execute() + .get() + .getResults(); + assertThat(data(results)) + .isEqualTo( + Lists.newArrayList( + map( + "second_tag", + "space", + "merged_awards", + map("hugo", true, "nebula", false, "new_award", true)))); + + results = + firestore + .pipeline() + .createFrom(collection) + .where(equal("title", "The Hitchhiker's Guide to the Galaxy")) + .select( + arrayReverse("tags").as("reversed_tags"), + mapRemove(field("awards"), "nebula").as("removed_awards")) + .execute() + .get() + .getResults(); + assertThat(data(results)) + .isEqualTo( + Lists.newArrayList( + map( + "reversed_tags", + Lists.newArrayList("adventure", "space", "comedy"), + "removed_awards", + map("hugo", true)))); + } + + @Test + public void testTimestampTrunc() throws Exception { + List results = + firestore + .pipeline() + .collection(collection.getPath()) + .where(equal("title", "Timestamp Book")) + .select( + Expression.timestampTruncate(field("timestamp"), "year").as("trunc_year"), + Expression.timestampTruncate(field("timestamp"), "month").as("trunc_month"), + Expression.timestampTruncate(field("timestamp"), "day").as("trunc_day"), + Expression.timestampTruncate(field("timestamp"), "hour").as("trunc_hour"), + Expression.timestampTruncate(field("timestamp"), "minute").as("trunc_minute"), + Expression.timestampTruncate(field("timestamp"), "second").as("trunc_second")) + .execute() + .get() + .getResults(); + assertThat(results).hasSize(1); + Map data = results.get(0).getData(); + Date originalDate = (Date) bookDocs.get("book11").get("timestamp"); + java.util.Calendar cal = java.util.Calendar.getInstance(java.util.TimeZone.getTimeZone("UTC")); + cal.setTime(originalDate); + + cal.set(java.util.Calendar.MONTH, java.util.Calendar.JANUARY); + cal.set(java.util.Calendar.DAY_OF_MONTH, 1); + cal.set(java.util.Calendar.HOUR_OF_DAY, 0); + cal.set(java.util.Calendar.MINUTE, 0); + cal.set(java.util.Calendar.SECOND, 0); + cal.set(java.util.Calendar.MILLISECOND, 0); + assertThat(data.get("trunc_year")).isEqualTo(Timestamp.of(cal.getTime())); + + cal.setTime(originalDate); + cal.set(java.util.Calendar.DAY_OF_MONTH, 1); + cal.set(java.util.Calendar.HOUR_OF_DAY, 0); + cal.set(java.util.Calendar.MINUTE, 0); + cal.set(java.util.Calendar.SECOND, 0); + cal.set(java.util.Calendar.MILLISECOND, 0); + assertThat(data.get("trunc_month")).isEqualTo(Timestamp.of(cal.getTime())); + + cal.setTime(originalDate); + cal.set(java.util.Calendar.HOUR_OF_DAY, 0); + cal.set(java.util.Calendar.MINUTE, 0); + cal.set(java.util.Calendar.SECOND, 0); + cal.set(java.util.Calendar.MILLISECOND, 0); + assertThat(data.get("trunc_day")).isEqualTo(Timestamp.of(cal.getTime())); + + cal.setTime(originalDate); + cal.set(java.util.Calendar.MINUTE, 0); + cal.set(java.util.Calendar.SECOND, 0); + cal.set(java.util.Calendar.MILLISECOND, 0); + assertThat(data.get("trunc_hour")).isEqualTo(Timestamp.of(cal.getTime())); + + cal.setTime(originalDate); + cal.set(java.util.Calendar.SECOND, 0); + cal.set(java.util.Calendar.MILLISECOND, 0); + assertThat(data.get("trunc_minute")).isEqualTo(Timestamp.of(cal.getTime())); + + cal.setTime(originalDate); + cal.set(java.util.Calendar.MILLISECOND, 0); + assertThat(data.get("trunc_second")).isEqualTo(Timestamp.of(cal.getTime())); + } + + @Test + public void testMathExpressions() throws Exception { + List results = + firestore + .pipeline() + .createFrom(collection) + .where(equal("title", "The Hitchhiker's Guide to the Galaxy")) + .select( + ceil(field("rating")).as("ceil_rating"), + floor(field("rating")).as("floor_rating"), + pow(field("rating"), 2).as("pow_rating"), + round(field("rating")).as("round_rating"), + sqrt(field("rating")).as("sqrt_rating"), + field("published").mod(10).as("mod_published")) + .execute() + .get() + .getResults(); + Map result = data(results).get(0); + assertThat((Double) result.get("ceil_rating")).isEqualTo(5.0); + assertThat((Double) result.get("floor_rating")).isEqualTo(4.0); + assertThat((Double) result.get("pow_rating")).isWithin(0.00001).of(17.64); + assertThat((Double) result.get("round_rating")).isEqualTo(4.0); + assertThat((Double) result.get("sqrt_rating")).isWithin(0.00001).of(2.04939); + assertThat((Long) result.get("mod_published")).isEqualTo(9L); + } + + @Test + public void testAdvancedMathExpressions() throws Exception { + List results = + firestore + .pipeline() + .createFrom(collection) + .where(equal("title", "The Lord of the Rings")) + .select( + exp(field("rating")).as("exp_rating"), + ln(field("rating")).as("ln_rating"), + log(field("rating"), 10).as("log_rating"), + field("rating").log10().as("log10_rating")) + .execute() + .get() + .getResults(); + Map result = data(results).get(0); + assertThat((Double) result.get("exp_rating")).isWithin(0.00001).of(109.94717); + assertThat((Double) result.get("ln_rating")).isWithin(0.00001).of(1.54756); + assertThat((Double) result.get("log_rating")).isWithin(0.00001).of(0.67209); + assertThat((Double) result.get("log10_rating")).isWithin(0.00001).of(0.67209); + } + + @Test + public void testConcat() throws Exception { + // String concat + List results = + firestore + .pipeline() + .collection(collection.getPath()) + .where(equal("title", "The Hitchhiker's Guide to the Galaxy")) + .select(concat(field("author"), " ", field("title")).as("author_title")) + .execute() + .get() + .getResults(); + Map result = data(results).get(0); + assertThat(result.get("author_title")) + .isEqualTo("Douglas Adams The Hitchhiker's Guide to the Galaxy"); + + // Array concat + results = + firestore + .pipeline() + .collection(collection.getPath()) + .where(equal("title", "The Hitchhiker's Guide to the Galaxy")) + .select(concat(field("tags"), ImmutableList.of("newTag")).as("new_tags")) + .execute() + .get() + .getResults(); + result = data(results).get(0); + assertThat((List) result.get("new_tags")) + .containsExactly("comedy", "space", "adventure", "newTag"); + + // Blob concat + byte[] bytes1 = new byte[] {1, 2}; + byte[] bytes2 = new byte[] {3, 4}; + byte[] expected = new byte[] {1, 2, 3, 4}; + results = + firestore + .pipeline() + .collection(collection.getPath()) + .limit(1) + .select( + concat( + constant(com.google.cloud.firestore.Blob.fromBytes(bytes1)), + com.google.cloud.firestore.Blob.fromBytes(bytes2)) + .as("concatenated_blob")) + .execute() + .get() + .getResults(); + result = data(results).get(0); + assertThat(((com.google.cloud.firestore.Blob) result.get("concatenated_blob")).toBytes()) + .isEqualTo(expected); + + // Mismatched types should just fail. + assertThrows( + ExecutionException.class, + () -> + firestore + .pipeline() + .collection(collection.getPath()) + .where(equal("title", "The Hitchhiker's Guide to the Galaxy")) + .select(concat(field("title"), field("tags")).as("mismatched")) + .execute() + .get() + .getResults()); + } + + @Test + public void testCurrentTimestamp() throws Exception { + List results = + firestore + .pipeline() + .collection(collection.getPath()) + .limit(1) + .select(Expression.currentTimestamp().as("now")) + .execute() + .get() + .getResults(); + assertThat(results).hasSize(1); + Object nowValue = results.get(0).getData().get("now"); + assertThat(nowValue).isInstanceOf(Timestamp.class); + Timestamp nowTimestamp = (Timestamp) nowValue; + // Check that the timestamp is recent (e.g., within the last 5 seconds) + long diff = new Date().getTime() - nowTimestamp.toDate().getTime(); + assertThat(diff).isAtMost(5000L); + } + + @Test + public void testIfAbsent() throws Exception { + // Case 1: Field is present, should return the field value. + List results = + firestore + .pipeline() + .collection(collection.getPath()) + .where(equal("title", "The Hitchhiker's Guide to the Galaxy")) + .select(field("rating").ifAbsent(0.0).as("rating_or_default")) + .execute() + .get() + .getResults(); + assertThat(data(results)).containsExactly(map("rating_or_default", 4.2)); + + // Case 2: Field is absent, should return the default value. + results = + firestore + .pipeline() + .collection(collection.getPath()) + .where(equal("title", "The Hitchhiker's Guide to the Galaxy")) + .select( + Expression.ifAbsent(field("non_existent_field"), "default").as("field_or_default")) + .execute() + .get() + .getResults(); + assertThat(data(results)).containsExactly(map("field_or_default", "default")); + + // Case 3: Field is present and null, should return null. + collection + .document("bookWithNull") + .set(map("title", "Book With Null", "optional_field", null)) + .get(); + results = + firestore + .pipeline() + .collection(collection.getPath()) + .where(equal("title", "Book With Null")) + .select(Expression.ifAbsent(field("optional_field"), "default").as("field_or_default")) + .execute() + .get() + .getResults(); + assertThat(results.get(0).get("field_or_default")).isNull(); + collection.document("bookWithNull").delete().get(); + + // Case 4: Test different overloads. + // ifAbsent(String, Any) + results = + firestore + .pipeline() + .collection(collection.getPath()) + .where(equal("title", "Dune")) + .select(Expression.ifAbsent("non_existent_field", "default_string").as("res")) + .execute() + .get() + .getResults(); + assertThat(data(results)).containsExactly(map("res", "default_string")); + + // ifAbsent(String, Expression) + results = + firestore + .pipeline() + .collection(collection.getPath()) + .where(equal("title", "Dune")) + .select(Expression.ifAbsent("non_existent_field", field("author")).as("res")) + .execute() + .get() + .getResults(); + assertThat(data(results)).containsExactly(map("res", "Frank Herbert")); + + // ifAbsent(Expression, Expression) + results = + firestore + .pipeline() + .collection(collection.getPath()) + .where(equal("title", "Dune")) + .select(Expression.ifAbsent(field("non_existent_field"), field("author")).as("res")) + .execute() + .get() + .getResults(); + assertThat(data(results)).containsExactly(map("res", "Frank Herbert")); + } + + @Test + public void testJoin() throws Exception { + // Test join with a constant delimiter + List results = + firestore + .pipeline() + .collection(collection.getPath()) + .where(equal("title", "The Hitchhiker's Guide to the Galaxy")) + .select(Expression.join("tags", ", ").as("joined_tags")) + .execute() + .get() + .getResults(); + Map result = data(results).get(0); + assertThat(result.get("joined_tags")).isEqualTo("comedy, space, adventure"); + + // Test join with an expression delimiter + results = + firestore + .pipeline() + .collection(collection.getPath()) + .where(equal("title", "The Hitchhiker's Guide to the Galaxy")) + .select(Expression.join(field("tags"), constant(" | ")).as("joined_tags")) + .execute() + .get() + .getResults(); + result = data(results).get(0); + assertThat(result.get("joined_tags")).isEqualTo("comedy | space | adventure"); + + // Test extension method + results = + firestore + .pipeline() + .collection(collection.getPath()) + .where(equal("title", "The Hitchhiker's Guide to the Galaxy")) + .select(field("tags").join(" - ").as("joined_tags")) + .execute() + .get() + .getResults(); + result = data(results).get(0); + assertThat(result.get("joined_tags")).isEqualTo("comedy - space - adventure"); + } + + @Test + public void testArraySum() throws Exception { + collection.document("book4").update("sales", ImmutableList.of(100, 200, 50)).get(); + List results = + firestore + .pipeline() + .collection(collection.getPath()) + .where(equal("title", "The Lord of the Rings")) + .select(Expression.arraySum("sales").as("totalSales")) + .limit(1) + .execute() + .get() + .getResults(); + assertThat(data(results)).containsExactly(map("totalSales", 350L)); + } + + @Test + public void testTimestampConversions() throws Exception { + List results = + firestore + .pipeline() + .collection(collection.getPath()) + .limit(1) + .select( + unixSecondsToTimestamp(constant(1741380235L)).as("unixSecondsToTimestamp"), + unixMillisToTimestamp(constant(1741380235123L)).as("unixMillisToTimestamp"), + unixMicrosToTimestamp(constant(1741380235123456L)).as("unixMicrosToTimestamp"), + timestampToUnixSeconds( + constant(Timestamp.ofTimeSecondsAndNanos(1741380235L, 123456789))) + .as("timestampToUnixSeconds"), + timestampToUnixMicros( + constant(Timestamp.ofTimeSecondsAndNanos(1741380235L, 123456789))) + .as("timestampToUnixMicros"), + timestampToUnixMillis( + constant(Timestamp.ofTimeSecondsAndNanos(1741380235L, 123456789))) + .as("timestampToUnixMillis")) + .execute() + .get() + .getResults(); + Map result = data(results).get(0); + assertThat(result.get("unixSecondsToTimestamp")) + .isEqualTo(Timestamp.ofTimeSecondsAndNanos(1741380235L, 0)); + assertThat(result.get("unixMillisToTimestamp")) + .isEqualTo(Timestamp.ofTimeSecondsAndNanos(1741380235L, 123000000)); + assertThat(result.get("unixMicrosToTimestamp")) + .isEqualTo(Timestamp.ofTimeSecondsAndNanos(1741380235L, 123456000)); + assertThat(result.get("timestampToUnixSeconds")).isEqualTo(1741380235L); + assertThat(result.get("timestampToUnixMicros")).isEqualTo(1741380235123456L); + assertThat(result.get("timestampToUnixMillis")).isEqualTo(1741380235123L); + } + + @Test + public void testVectorLength() throws Exception { + List results = + firestore + .pipeline() + .collection(collection.getPath()) + .limit(1) + .select(vectorLength(vector(new double[] {1.0, 2.0, 3.0})).as("vectorLength")) + .execute() + .get() + .getResults(); + assertThat(data(results)).isEqualTo(Lists.newArrayList(map("vectorLength", 3L))); + } + + @Test + public void testStrContains() throws Exception { + List results = + firestore + .pipeline() + .createFrom(collection) + .where(Expression.stringContains(field("title"), "'s")) + .select("title") + .sort(field("title").ascending()) + .execute() + .get() + .getResults(); + assertThat(data(results)) + .containsExactly( + map("title", "The Handmaid's Tale"), + map("title", "The Hitchhiker's Guide to the Galaxy")); + } + + @Test + public void testSubstring() throws Exception { + List results = + firestore + .pipeline() + .createFrom(collection) + .where(equal("title", "The Lord of the Rings")) + .select( + Expression.substring(field("title"), constant(9), constant(2)).as("of"), + substring("title", 16, 5).as("Rings")) + .execute() + .get() + .getResults(); + assertThat(data(results)).isEqualTo(Lists.newArrayList(map("of", "of", "Rings", "Rings"))); + } + + @Test + public void testSplitStringByStringDelimiter() throws Exception { + List results = + firestore + .pipeline() + .collection(collection.getPath()) + .where(equal("title", "The Hitchhiker's Guide to the Galaxy")) + .select(Expression.split(field("title"), " ").as("split_title")) + .execute() + .get() + .getResults(); + assertThat(data(results)) + .containsExactly( + map( + "split_title", + ImmutableList.of("The", "Hitchhiker's", "Guide", "to", "the", "Galaxy"))); + + results = + firestore + .pipeline() + .collection(collection.getPath()) + .where(equal("title", "The Hitchhiker's Guide to the Galaxy")) + .select(field("title").split(" ").as("split_title")) + .execute() + .get() + .getResults(); + assertThat(data(results)) + .containsExactly( + map( + "split_title", + ImmutableList.of("The", "Hitchhiker's", "Guide", "to", "the", "Galaxy"))); + } + + @Test + public void testSplitStringByExpressionDelimiter() throws Exception { + List results = + firestore + .pipeline() + .collection(collection.getPath()) + .where(equal("title", "The Hitchhiker's Guide to the Galaxy")) + .select(Expression.split(field("title"), constant(" ")).as("split_title")) + .execute() + .get() + .getResults(); + assertThat(data(results)) + .containsExactly( + map( + "split_title", + ImmutableList.of("The", "Hitchhiker's", "Guide", "to", "the", "Galaxy"))); + + results = + firestore + .pipeline() + .collection(collection.getPath()) + .where(equal("title", "The Hitchhiker's Guide to the Galaxy")) + .select(field("title").split(constant(" ")).as("split_title")) + .execute() + .get() + .getResults(); + assertThat(data(results)) + .containsExactly( + map( + "split_title", + ImmutableList.of("The", "Hitchhiker's", "Guide", "to", "the", "Galaxy"))); + } + + @Test + public void testSplitBlobByByteArrayDelimiter() throws Exception { + List results = + firestore + .pipeline() + .collection(collection.getPath()) + .limit(1) + .addFields( + constant(Blob.fromBytes(new byte[] {0x01, 0x02, 0x03, 0x04, 0x01, 0x05})) + .as("data")) + .select( + Expression.split(field("data"), constant(Blob.fromBytes(new byte[] {0x01}))) + .as("split_data")) + .execute() + .get() + .getResults(); + assertThat(data(results)) + .containsExactly( + map( + "split_data", + ImmutableList.of( + Blob.fromBytes(new byte[] {}), + Blob.fromBytes(new byte[] {0x02, 0x03, 0x04}), + Blob.fromBytes(new byte[] {0x05})))); + + results = + firestore + .pipeline() + .collection(collection.getPath()) + .limit(1) + .addFields( + constant(Blob.fromBytes(new byte[] {0x01, 0x02, 0x03, 0x04, 0x01, 0x05})) + .as("data")) + .select( + field("data").split(constant(Blob.fromBytes(new byte[] {0x01}))).as("split_data")) + .execute() + .get() + .getResults(); + assertThat(data(results)) + .containsExactly( + map( + "split_data", + ImmutableList.of( + Blob.fromBytes(new byte[] {}), + Blob.fromBytes(new byte[] {0x02, 0x03, 0x04}), + Blob.fromBytes(new byte[] {0x05})))); + } + + @Test + public void testSplitStringFieldByStringDelimiter() throws Exception { + List results = + firestore + .pipeline() + .collection(collection.getPath()) + .where(equal("title", "The Hitchhiker's Guide to the Galaxy")) + .select(Expression.split("title", " ").as("split_title")) + .execute() + .get() + .getResults(); + assertThat(data(results)) + .containsExactly( + map( + "split_title", + ImmutableList.of("The", "Hitchhiker's", "Guide", "to", "the", "Galaxy"))); + } + + @Test + public void testSplitStringFieldByExpressionDelimiter() throws Exception { + List results = + firestore + .pipeline() + .collection(collection.getPath()) + .where(equal("title", "The Hitchhiker's Guide to the Galaxy")) + .select(Expression.split("title", constant(" ")).as("split_title")) + .execute() + .get() + .getResults(); + assertThat(data(results)) + .containsExactly( + map( + "split_title", + ImmutableList.of("The", "Hitchhiker's", "Guide", "to", "the", "Galaxy"))); + } + + @Test + public void testSplitWithMismatchedTypesShouldFail() { + ExecutionException exception = + assertThrows( + ExecutionException.class, + () -> + firestore + .pipeline() + .collection(collection.getPath()) + .where(equal("title", "The Hitchhiker's Guide to the Galaxy")) + .select( + Expression.split( + field("title"), constant(Blob.fromBytes(new byte[] {0x01}))) + .as("mismatched_split")) + .execute() + .get()); + assertThat(exception.getCause()).isInstanceOf(ApiException.class); + ApiException apiException = (ApiException) exception.getCause(); + assertThat(apiException.getStatusCode().getCode()).isEqualTo(StatusCode.Code.INVALID_ARGUMENT); + } + + @Test + public void testDistanceFunctions() throws Exception { + double[] sourceVector = {0.1, 0.1}; + double[] targetVector = {0.5, 0.8}; + List results = + firestore + .pipeline() + .collection(collection.getPath()) + .select( + cosineDistance(vector(sourceVector), targetVector).as("cosineDistance"), + dotProduct(vector(sourceVector), targetVector).as("dotProductDistance"), + euclideanDistance(vector(sourceVector), targetVector).as("euclideanDistance")) + .limit(1) + .execute() + .get() + .getResults(); + + assertThat(data(results)) + .isEqualTo( + Lists.newArrayList( + map( + "cosineDistance", 0.02560880430538015, + "dotProductDistance", 0.13, + "euclideanDistance", 0.806225774829855))); + } + + @Test + public void testNestedFields() throws Exception { + List results = + firestore + .pipeline() + .createFrom(collection) + .where(equal("awards.hugo", true)) + .select("title", "awards.hugo") + .execute() + .get() + .getResults(); + + assertThat(data(results)) + .isEqualTo( + Lists.newArrayList( + map("title", "The Hitchhiker's Guide to the Galaxy", "awards.hugo", true), + map("title", "Dune", "awards.hugo", true))); + } + + @Test + public void testPipelineInTransactions() throws Exception { + assumeFalse( + "Transactions are not supported against the emulator.", + isRunningAgainstFirestoreEmulator(firestore)); + Pipeline pipeline = + firestore + .pipeline() + .createFrom(collection) + .where(equal("awards.hugo", true)) + .select("title", "awards.hugo", Field.DOCUMENT_ID); + + firestore + .runTransaction( + transaction -> { + List results = transaction.execute(pipeline).get().getResults(); + + assertThat(data(results)) + .isEqualTo( + Lists.newArrayList( + map("title", "The Hitchhiker's Guide to the Galaxy", "awards.hugo", true), + map("title", "Dune", "awards.hugo", true))); + + transaction.update(collection.document("book1"), map("foo", "bar")); + + return "done"; + }) + .get(); + + List result = + firestore + .pipeline() + .createFrom(collection) + .where(equal("foo", "bar")) + .select("title") + .execute() + .get() + .getResults(); + assertThat(data(result)) + .isEqualTo(Lists.newArrayList(map("title", "The Hitchhiker's Guide to the Galaxy"))); + } + + @Test + public void testPipelineInTransactionsWithOptions() throws Exception { + assumeFalse( + "Transactions are not supported against the emulator.", + isRunningAgainstFirestoreEmulator(firestore)); + Pipeline pipeline = firestore.pipeline().createFrom(collection).limit(1); + + firestore + .runTransaction( + transaction -> { + PipelineExecuteOptions options = new PipelineExecuteOptions().with("foo", "bar"); + List results = + transaction.execute(pipeline, options).get().getResults(); + assertThat(results).hasSize(1); + return "done"; + }) + .get(); + } + + @Test + public void testRawStage() throws Exception { + // can select fields + List results = + firestore + .pipeline() + .collection(collection.getPath()) + // .select(field("title"), Expression.map(map("author", + // field("author"))).as("metadata")) + .rawStage( + RawStage.ofName("select") + .withArguments( + map( + "title", + field("title"), + "metadata", + Expression.map(map("author", field("author")))))) + .sort(field("metadata.author").ascending()) + .limit(1) + .execute() + .get() + .getResults(); + + assertThat(data(results)) + .isEqualTo( + Lists.newArrayList( + map( + "title", + "The Hitchhiker's Guide to the Galaxy", + "metadata", + map("author", "Douglas Adams")))); + + // can add fields + results = + firestore + .pipeline() + .collection(collection.getPath()) + .sort(field("author").ascending()) + .limit(1) + .select("title", "author") + .rawStage( + RawStage.ofName("add_fields") + .withArguments( + map( + "display", + Expression.stringConcat(field("title"), " - ", field("author"))))) + .execute() + .get() + .getResults(); + assertThat(data(results)) + .isEqualTo( + Lists.newArrayList( + map( + "title", + "The Hitchhiker's Guide to the Galaxy", + "author", + "Douglas Adams", + "display", + "The Hitchhiker's Guide to the Galaxy - Douglas Adams"))); + + // can filter with where + results = + firestore + .pipeline() + .collection(collection.getPath()) + .select("title", "author") + .rawStage(RawStage.ofName("where").withArguments(equal("author", "Douglas Adams"))) + .execute() + .get() + .getResults(); + assertThat(data(results)) + .isEqualTo( + Lists.newArrayList( + map("title", "The Hitchhiker's Guide to the Galaxy", "author", "Douglas Adams"))); + + // can limit, offset, and sort + results = + firestore + .pipeline() + .collection(collection.getPath()) + .select("title", "author") + .rawStage( + RawStage.ofName("sort") + .withArguments(map("direction", "ascending", "expression", field("author")))) + .rawStage(RawStage.ofName("offset").withArguments(3)) + .rawStage(RawStage.ofName("limit").withArguments(1)) + .execute() + .get() + .getResults(); + assertThat(data(results)) + .isEqualTo( + Lists.newArrayList( + map("author", "Fyodor Dostoevsky", "title", "Crime and Punishment"))); + + // can perform aggregate query + results = + firestore + .pipeline() + .collection(collection.getPath()) + .select("title", "author", "rating") + .rawStage( + RawStage.ofName("aggregate") + .withArguments( + map("averageRating", AggregateFunction.average("rating")), map())) + .execute() + .get() + .getResults(); + Map aggregateResult = data(results).get(0); + assertThat((Double) aggregateResult.get("averageRating")).isWithin(0.00001).of(4.31); + + // can perform distinct query + results = + firestore + .pipeline() + .collection(collection.getPath()) + .select("title", "author", "rating") + .rawStage(RawStage.ofName("distinct").withArguments(map("rating", field("rating")))) + .sort(field("rating").descending()) + .execute() + .get() + .getResults(); + assertThat(data(results)) + .containsExactly( + map("rating", 4.7), + map("rating", 4.6), + map("rating", 4.5), + map("rating", 4.3), + map("rating", 4.2), + map("rating", 4.1), + map("rating", 4.0), + map("rating", null)); + + // can perform FindNearest query + results = + firestore + .pipeline() + .collection(collection.getPath()) + .rawStage( + RawStage.ofName("find_nearest") + .withArguments( + field("embedding"), + vector(new double[] {10.0, 1.0, 2.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0}), + "euclidean") + .withOptions( + new RawOptions() + .with("distance_field", field("computedDistance")) + .with("limit", 2))) + .select("title", "computedDistance") + .execute() + .get() + .getResults(); + assertThat(results.size()).isEqualTo(2); + assertThat(results.get(0).getData().get("title")) + .isEqualTo("The Hitchhiker's Guide to the Galaxy"); + assertThat((Double) results.get(0).getData().get("computedDistance")).isWithin(0.00001).of(1.0); + assertThat(results.get(1).getData().get("title")).isEqualTo("One Hundred Years of Solitude"); + assertThat((Double) results.get(1).getData().get("computedDistance")) + .isWithin(0.00001) + .of(12.041594578792296); + } + + @Test + public void testReplaceWith() throws Exception { + List results = + firestore + .pipeline() + .createFrom(collection) + .where(equal("title", "The Hitchhiker's Guide to the Galaxy")) + .replaceWith("awards") + .execute() + .get() + .getResults(); + assertThat(data(results)).isEqualTo(Lists.newArrayList(map("hugo", true, "nebula", false))); + + results = + firestore + .pipeline() + .createFrom(collection) + .where(equal("title", "The Hitchhiker's Guide to the Galaxy")) + .replaceWith( + Expression.map( + map("foo", "bar", "baz", Expression.map(map("title", field("title")))))) + .execute() + .get() + .getResults(); + assertThat(data(results)) + .isEqualTo( + Lists.newArrayList( + map("foo", "bar", "baz", map("title", "The Hitchhiker's Guide to the Galaxy")))); + } + + @Test + public void testSampleLimit() throws Exception { + List results = + firestore.pipeline().createFrom(collection).sample(3).execute().get().getResults(); + + assertThat(results).hasSize(3); + } + + @Test + public void testSamplePercentage() throws Exception { + List results = + firestore + .pipeline() + .createFrom(collection) + .sample(Sample.withPercentage(0.6)) + .execute() + .get() + .getResults(); + + assertThat(results).isNotEmpty(); + } + + @Test + public void testUnion() throws Exception { + List results = + firestore + .pipeline() + .createFrom(collection) + .union(firestore.pipeline().createFrom(collection)) + .execute() + .get() + .getResults(); + + assertThat(results).hasSize(22); + } + + @Test + public void testUnnest() throws Exception { + List results = + firestore + .pipeline() + .createFrom(collection) + .where(Expression.equal(field("title"), "The Hitchhiker's Guide to the Galaxy")) + .unnest("tags", "tag") + .execute() + .get() + .getResults(); + + assertThat(results).hasSize(3); + } + + @Test + public void testUnnestWithIndexField() throws Exception { + List results = + firestore + .pipeline() + .createFrom(collection) + .where(Expression.equal(field("title"), "The Hitchhiker's Guide to the Galaxy")) + .unnest("tags", "tag", new UnnestOptions().withIndexField("tagsIndex")) + .execute() + .get() + .getResults(); + + assertThat(results).hasSize(3); + for (int i = 0; i < results.size(); i++) { + assertThat(results.get(i).getData().get("tagsIndex")).isEqualTo((long) i); + } + } + + @Test + public void testUnnestWithExpr() throws Exception { + List results = + firestore + .pipeline() + .createFrom(collection) + .where(Expression.equal(field("title"), "The Hitchhiker's Guide to the Galaxy")) + .unnest(array(1L, 2L, 3L).as("copy")) + .execute() + .get() + .getResults(); + + assertThat(results).hasSize(3); + for (int i = 0; i < results.size(); i++) { + assertThat(results.get(i).getData().get("copy")).isEqualTo((long) i + 1); + } + } + + @Test + public void testPaginationWithStartAfter() throws Exception { + CollectionReference paginationCollection = + testCollectionWithDocs( + ImmutableMap.>builder() + .put("doc1", map("order", 1)) + .put("doc2", map("order", 2)) + .put("doc3", map("order", 3)) + .put("doc4", map("order", 4)) + .build()); + + Pipeline pipeline = + firestore.pipeline().createFrom(paginationCollection.orderBy("order").limit(2)); + + Pipeline.Snapshot snapshot = pipeline.execute().get(); + assertThat(data(snapshot.getResults())).containsExactly(map("order", 1L), map("order", 2L)); + + PipelineResult lastResult = snapshot.getResults().get(snapshot.getResults().size() - 1); + snapshot = + firestore + .pipeline() + .createFrom(paginationCollection.orderBy("order").startAfter(lastResult.get("order"))) + .execute() + .get(); + assertThat(data(snapshot.getResults())).containsExactly(map("order", 3L), map("order", 4L)); + } + + @Test + public void testDocumentsAsSource() throws Exception { + List results = + firestore + .pipeline() + .documents( + collection.document("book1"), + collection.document("book2"), + collection.document("book3")) + .execute() + .get() + .getResults(); + assertThat(results).hasSize(3); + } + + @Test + public void testCollectionGroupAsSource() throws Exception { + String subcollectionId = LocalFirestoreHelper.autoId(); + collection.document("book1").collection(subcollectionId).add(map("order", 1)).get(); + collection.document("book2").collection(subcollectionId).add(map("order", 2)).get(); + List results = + firestore + .pipeline() + .collectionGroup(subcollectionId) + .sort(field("order").ascending()) + .execute() + .get() + .getResults(); + assertThat(data(results)).isEqualTo(Lists.newArrayList(map("order", 1L), map("order", 2L))); + } + + @Test + public void testDatabaseAsSource() throws Exception { + String randomId = LocalFirestoreHelper.autoId(); + collection.document("book1").collection("sub").add(map("order", 1, "randomId", randomId)).get(); + collection.document("book2").collection("sub").add(map("order", 2, "randomId", randomId)).get(); + List results = + firestore + .pipeline() + .database() + .where(equal("randomId", randomId)) + .sort(field("order").ascending()) + .execute() + .get() + .getResults(); + assertThat(data(results)) + .isEqualTo( + Lists.newArrayList( + map("order", 1L, "randomId", randomId), map("order", 2L, "randomId", randomId))); + } + + @Test + public void testFindNearest() throws Exception { + List results = + firestore + .pipeline() + .collection(collection.getPath()) + .findNearest( + "embedding", + new double[] {10.0, 1.0, 2.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0}, + FindNearest.DistanceMeasure.EUCLIDEAN, + new FindNearestOptions().withLimit(2).withDistanceField("computedDistance")) + .select("title", "computedDistance") + .execute() + .get() + .getResults(); + assertThat(results.size()).isEqualTo(2); + assertThat(results.get(0).getData().get("title")) + .isEqualTo("The Hitchhiker's Guide to the Galaxy"); + assertThat((Double) results.get(0).getData().get("computedDistance")).isWithin(0.00001).of(1.0); + assertThat(results.get(1).getData().get("title")).isEqualTo("One Hundred Years of Solitude"); + assertThat((Double) results.get(1).getData().get("computedDistance")) + .isWithin(0.00001) + .of(12.041594578792296); + } + + @Test + public void testExplain() throws Exception { + assumeFalse( + "Explain is not supported against the emulator.", + isRunningAgainstFirestoreEmulator(firestore)); + Pipeline pipeline = + firestore.pipeline().createFrom(collection).sort(field("__name__").ascending()); + Pipeline.Snapshot snapshot = + pipeline + .execute( + new PipelineExecuteOptions() + .withExplainOptions( + new ExplainOptions() + .withExecutionMode(ExplainOptions.ExecutionMode.ANALYZE))) + .get(); + assertThat(snapshot.getResults()).isNotEmpty(); + assertThat(snapshot.getExplainStats().getText()).isNotEmpty(); + + snapshot = + pipeline + .execute(new PipelineExecuteOptions().withExplainOptions(new ExplainOptions())) + .get(); + assertThat(snapshot.getResults()).isNotEmpty(); + assertThat(snapshot.getExplainStats()).isNull(); + } + + @Test + public void testOptions() throws ExecutionException, InterruptedException { + assumeFalse( + "Certain options are not supported against the emulator.", + isRunningAgainstFirestoreEmulator(firestore)); + PipelineExecuteOptions opts = + new PipelineExecuteOptions() + .withIndexMode("recommended") + .withExplainOptions( + new ExplainOptions().withExecutionMode(ExplainOptions.ExecutionMode.ANALYZE)); + + double[] vector = {1.0, 2.0, 3.0}; + + Pipeline pipeline = + firestore + .pipeline() + .collection( + "/k", + new CollectionOptions().withHints(new CollectionHints().withForceIndex("title"))) + .findNearest( + "topicVectors", + vector, + FindNearest.DistanceMeasure.COSINE, + new FindNearestOptions().withLimit(10).withDistanceField("distance")) + .aggregate( + Aggregate.withAccumulators(AggregateFunction.average("rating").as("avg_rating")) + .withGroups("genre"), + new AggregateOptions() + .withHints(new AggregateHints().withForceStreamableEnabled())); + + pipeline.execute(opts).get(); + } + + @Test + public void testErrorHandling() { + assumeFalse( + "Error handling is not supported against the emulator.", + isRunningAgainstFirestoreEmulator(firestore)); + ExecutionException exception = + assertThrows( + ExecutionException.class, + () -> { + firestore + .pipeline() + .collection(collection.getPath()) + .rawStage(RawStage.ofName("invalidStage")) + .execute() + .get(); + }); + assertThat(exception.getCause()).isInstanceOf(ApiException.class); + ApiException apiException = (ApiException) exception.getCause(); + assertThat(apiException.getStatusCode().getCode()).isEqualTo(StatusCode.Code.INVALID_ARGUMENT); + } + + @Test + public void testType() throws Exception { + List results = + firestore + .pipeline() + .collection(collection.getPath()) + .where(field("author").equal("Douglas Adams")) + .limit(1) + .select( + Expression.type("title").as("string_type"), + Expression.type("published").as("number_type"), + Expression.type(field("awards").mapGet("hugo")).as("boolean_type"), + Expression.type(nullValue()).as("null_type"), + Expression.type("embedding").as("vector_type")) + .execute() + .get() + .getResults(); + assertThat(data(results)) + .containsExactly( + map( + "string_type", + "string", + "number_type", + "int64", + "boolean_type", + "boolean", + "null_type", + "null", + "vector_type", + "vector")); + } + + @Test + public void testExplainWithError() { + assumeFalse( + "Explain with error is not supported against the emulator.", + isRunningAgainstFirestoreEmulator(firestore)); + Pipeline pipeline = + firestore.pipeline().createFrom(collection).sort(field("rating").ascending()); + ExecutionException exception = + assertThrows( + ExecutionException.class, + () -> { + pipeline + .execute( + new PipelineExecuteOptions() + .withExplainOptions( + new ExplainOptions() + .withExecutionMode(ExplainOptions.ExecutionMode.ANALYZE)) + .with("memory_limit", 1)) + .get(); + }); + assertThat(exception.getCause()).isInstanceOf(ApiException.class); + ApiException apiException = (ApiException) exception.getCause(); + assertThat(apiException.getStatusCode().getCode()) + .isEqualTo(StatusCode.Code.RESOURCE_EXHAUSTED); + } + + @Test + public void testCrossDatabaseRejection() throws Exception { + FirestoreOptions firestoreOptions = + FirestoreOptions.newBuilder().setProjectId("test-project-2").build(); + try (Firestore firestore2 = firestoreOptions.getService()) { + CollectionReference collection2 = firestore2.collection("test-collection"); + IllegalArgumentException exception = + assertThrows( + IllegalArgumentException.class, + () -> { + firestore.pipeline().collection(collection2); + }); + assertThat(exception.getMessage()).contains("Invalid CollectionReference"); + } + } + + @Test + public void disallowDuplicateAliasesInAggregate() { + IllegalArgumentException exception = + assertThrows( + IllegalArgumentException.class, + () -> { + firestore + .pipeline() + .collection(collection.getPath()) + .aggregate(countAll().as("dup"), AggregateFunction.average("rating").as("dup")); + }); + assertThat(exception).hasMessageThat().contains("Duplicate alias or field name"); + } + + @Test + public void disallowDuplicateAliasesInSelect() { + IllegalArgumentException exception = + assertThrows( + IllegalArgumentException.class, + () -> { + firestore + .pipeline() + .collection(collection.getPath()) + .select(field("title").as("dup"), field("author").as("dup")); + }); + assertThat(exception).hasMessageThat().contains("Duplicate alias or field name"); + } + + @Test + public void disallowDuplicateAliasesInAddFields() { + IllegalArgumentException exception = + assertThrows( + IllegalArgumentException.class, + () -> { + firestore + .pipeline() + .collection(collection.getPath()) + .addFields(field("title").as("dup"), field("author").as("dup")); + }); + assertThat(exception).hasMessageThat().contains("Duplicate alias or field name"); + } + + @Test + public void disallowDuplicateAliasesInDistinct() { + IllegalArgumentException exception = + assertThrows( + IllegalArgumentException.class, + () -> { + firestore + .pipeline() + .collection(collection.getPath()) + .distinct(field("genre").as("dup"), field("author").as("dup")); + }); + assertThat(exception).hasMessageThat().contains("Duplicate alias or field name"); + } + + @Test + public void disallowDuplicateAliasesAcrossStages() { + IllegalArgumentException exception = + assertThrows( + IllegalArgumentException.class, + () -> { + firestore + .pipeline() + .collection(collection.getPath()) + .select(field("title").as("title_dup")) + .addFields(field("author").as("author_dup")) + .distinct(field("genre").as("genre_dup")) + .select(field("title_dup").as("final_dup"), field("author_dup").as("final_dup")); + }); + assertThat(exception).hasMessageThat().contains("Duplicate alias or field name"); + } +} diff --git a/google-cloud-firestore/src/test/java/com/google/cloud/firestore/it/ITQueryAggregationsTest.java b/google-cloud-firestore/src/test/java/com/google/cloud/firestore/it/ITQueryAggregationsTest.java index b2a6023cb..d3f13a9fb 100644 --- a/google-cloud-firestore/src/test/java/com/google/cloud/firestore/it/ITQueryAggregationsTest.java +++ b/google-cloud-firestore/src/test/java/com/google/cloud/firestore/it/ITQueryAggregationsTest.java @@ -30,6 +30,7 @@ import com.google.api.core.ApiFuture; import com.google.cloud.firestore.*; +import java.util.List; import java.util.Map; import java.util.concurrent.ExecutionException; import org.junit.Ignore; @@ -56,6 +57,24 @@ private CollectionReference testCollectionWithDocs(Map pipelineResults = + query.getQuery().getFirestore().pipeline().createFrom(query).execute().get().getResults(); + assertThat(pipelineResults).hasSize(1); + assertThat(pipelineResults.get(0).getData()) + .isEqualTo(TestUtil.getAggregateSnapshotData(snapshot)); + + return snapshot; + } + public static void writeAllDocs( CollectionReference collection, Map> docs) throws InterruptedException { @@ -74,12 +93,18 @@ public static void writeAllDocs( @Test public void canRunCountUsingAggregationMethod() throws Exception { CollectionReference collection = testCollectionWithDocs(testDocs1); - AggregateQuerySnapshot snapshot = collection.aggregate(AggregateField.count()).get().get(); + AggregateQuerySnapshot snapshot = + verifyPipelineReturnsSameResult(collection.aggregate(AggregateField.count())); assertThat(snapshot.getCount()).isEqualTo(2); } @Test public void allowsAliasesForLongestFieldNames() throws Exception { + assumeFalse( + "Skip this test when running against the Firestore emulator because it does not support" + + " long field names.", + isRunningAgainstFirestoreEmulator(firestore)); + // The longest field name allowed is 1499 characters long. // Ensure that sum(longestField) and average(longestField) work. StringBuilder builder = new StringBuilder(1500); @@ -92,10 +117,10 @@ public void allowsAliasesForLongestFieldNames() throws Exception { CollectionReference collection = testCollectionWithDocs(testDocs); AggregateQuerySnapshot snapshot = - collection.aggregate(AggregateField.sum(longestField)).get().get(); + verifyPipelineReturnsSameResult(collection.aggregate(AggregateField.sum(longestField))); assertThat(snapshot.get(AggregateField.sum(longestField))).isEqualTo(6); AggregateQuerySnapshot snapshot2 = - collection.aggregate(AggregateField.average(longestField)).get().get(); + verifyPipelineReturnsSameResult(collection.aggregate(AggregateField.average(longestField))); assertThat(snapshot2.get(AggregateField.average(longestField))).isEqualTo(3.0); } @@ -135,14 +160,16 @@ public void aggregateErrorMessageIfIndexIsMissing() throws Exception { @Test public void canRunSumQuery() throws Exception { CollectionReference collection = testCollectionWithDocs(testDocs1); - AggregateQuerySnapshot snapshot = collection.aggregate(sum("pages")).get().get(); + AggregateQuerySnapshot snapshot = + verifyPipelineReturnsSameResult(collection.aggregate(sum("pages"))); assertThat(snapshot.get(sum("pages"))).isEqualTo(150); } @Test public void canRunAverageQuery() throws Exception { CollectionReference collection = testCollectionWithDocs(testDocs1); - AggregateQuerySnapshot snapshot = collection.aggregate(average("pages")).get().get(); + AggregateQuerySnapshot snapshot = + verifyPipelineReturnsSameResult(collection.aggregate(average("pages"))); assertThat(snapshot.get(average("pages"))).isEqualTo(75.0); } @@ -150,7 +177,8 @@ public void canRunAverageQuery() throws Exception { public void canGetMultipleAggregationsInTheSameQuery() throws Exception { CollectionReference collection = testCollectionWithDocs(testDocs1); AggregateQuerySnapshot snapshot = - collection.aggregate(sum("pages"), average("pages"), AggregateField.count()).get().get(); + verifyPipelineReturnsSameResult( + collection.aggregate(sum("pages"), average("pages"), AggregateField.count())); assertThat(snapshot.get(sum("pages"))).isEqualTo(150); assertThat(snapshot.get(average("pages"))).isEqualTo(75.0); assertThat(snapshot.get(AggregateField.count())).isEqualTo(2); @@ -160,7 +188,8 @@ public void canGetMultipleAggregationsInTheSameQuery() throws Exception { public void getCorrectTypeForSumLong() throws Exception { Map> testDocs = map("a", map("foo", 100), "b", map("foo", 100)); CollectionReference collection = testCollectionWithDocs(testDocs); - AggregateQuerySnapshot snapshot = collection.aggregate(sum("foo")).get().get(); + AggregateQuerySnapshot snapshot = + verifyPipelineReturnsSameResult(collection.aggregate(sum("foo"))); Object sum = snapshot.get(sum("foo")); assertThat(sum instanceof Long).isTrue(); } @@ -169,7 +198,8 @@ public void getCorrectTypeForSumLong() throws Exception { public void getCorrectTypeForSumDouble() throws Exception { Map> testDocs = map("a", map("foo", 100.5), "b", map("foo", 100)); CollectionReference collection = testCollectionWithDocs(testDocs); - AggregateQuerySnapshot snapshot = collection.aggregate(sum("foo")).get().get(); + AggregateQuerySnapshot snapshot = + verifyPipelineReturnsSameResult(collection.aggregate(sum("foo"))); Object sum = snapshot.get(sum("foo")); assertThat(sum instanceof Double).isTrue(); } @@ -179,7 +209,8 @@ public void getCorrectTypeForSumNaN() throws Exception { Map> testDocs = map("a", map("foo", 100.5), "b", map("foo", Double.NaN)); CollectionReference collection = testCollectionWithDocs(testDocs); - AggregateQuerySnapshot snapshot = collection.aggregate(sum("foo")).get().get(); + AggregateQuerySnapshot snapshot = + verifyPipelineReturnsSameResult(collection.aggregate(sum("foo"))); Object sum = snapshot.get(sum("foo")); assertThat(sum instanceof Double).isTrue(); assertThat(sum.equals(Double.NaN)); @@ -188,7 +219,8 @@ public void getCorrectTypeForSumNaN() throws Exception { @Test public void getCorrectTypeForAverageDouble() throws Exception { CollectionReference collection = testCollectionWithDocs(testDocs1); - AggregateQuerySnapshot snapshot = collection.aggregate(average("pages")).get().get(); + AggregateQuerySnapshot snapshot = + verifyPipelineReturnsSameResult(collection.aggregate(average("pages"))); Object average = snapshot.get((AggregateField) average("pages")); assertThat(average instanceof Double).isTrue(); } @@ -198,7 +230,8 @@ public void getCorrectTypeForAverageNaN() throws Exception { Map> testDocs = map("a", map("foo", 100.5), "b", map("foo", Double.NaN)); CollectionReference collection = testCollectionWithDocs(testDocs); - AggregateQuerySnapshot snapshot = collection.aggregate(average("foo")).get().get(); + AggregateQuerySnapshot snapshot = + verifyPipelineReturnsSameResult(collection.aggregate(average("foo"))); Object sum = snapshot.get(average("foo")); assertThat(sum instanceof Double).isTrue(); assertThat(sum.equals(Double.NaN)); @@ -207,7 +240,8 @@ public void getCorrectTypeForAverageNaN() throws Exception { @Test public void getCorrectTypeForAverageNull() throws Exception { CollectionReference collection = testCollection(); - AggregateQuerySnapshot snapshot = collection.aggregate(average("bar")).get().get(); + AggregateQuerySnapshot snapshot = + verifyPipelineReturnsSameResult(collection.aggregate(average("bar"))); Object sum = snapshot.get(average("bar")); assertThat(sum == null).isTrue(); } @@ -223,7 +257,8 @@ public void canPerformMaxAggregations() throws Exception { AggregateField f3 = AggregateField.count(); AggregateField f4 = sum("foo"); AggregateField f5 = sum("bar"); - AggregateQuerySnapshot snapshot = collection.aggregate(f1, f2, f3, f4, f5).get().get(); + AggregateQuerySnapshot snapshot = + verifyPipelineReturnsSameResult(collection.aggregate(f1, f2, f3, f4, f5)); assertThat(snapshot.get(f1)).isEqualTo(150); assertThat(snapshot.get(f2)).isEqualTo(75.0); assertThat(snapshot.get(f3)).isEqualTo(2); @@ -249,7 +284,9 @@ public void cannotPerformMoreThanMaxAggregations() throws Exception { exception = e; } assertThat(exception).isNotNull(); - assertThat(exception.getMessage()).contains("maximum number of aggregations"); + if (!isRunningAgainstFirestoreEmulator(firestore)) { + assertThat(exception.getMessage()).contains("maximum number of aggregations"); + } } @Test @@ -290,7 +327,8 @@ public void aggregateQueriesSupportCollectionGroups() throws Exception { .set(data)); CollectionGroup collectionGroup = firestore.collectionGroup(collectionGroupId); AggregateQuerySnapshot snapshot = - collectionGroup.aggregate(AggregateField.count(), sum("x"), average("x")).get().get(); + verifyPipelineReturnsSameResult( + collectionGroup.aggregate(AggregateField.count(), sum("x"), average("x"))); assertThat(snapshot.get(AggregateField.count())).isEqualTo(2); assertThat(snapshot.get(sum("x"))).isEqualTo(4); assertThat(snapshot.get(average("x"))).isEqualTo(2); @@ -313,10 +351,9 @@ public void performsAggregationsOnDocumentsWithAllAggregatedFields() throws Exce map("author", "authorD", "title", "titleD", "pages", 50)); CollectionReference collection = testCollectionWithDocs(testDocs); AggregateQuerySnapshot snapshot = - collection - .aggregate(sum("pages"), average("pages"), average("year"), AggregateField.count()) - .get() - .get(); + verifyPipelineReturnsSameResult( + collection.aggregate( + sum("pages"), average("pages"), average("year"), AggregateField.count())); assertThat(snapshot.get(sum("pages"))).isEqualTo(300); assertThat(snapshot.get(average("pages"))).isEqualTo(100); assertThat(snapshot.get(average("year"))).isEqualTo(2007); @@ -354,7 +391,8 @@ public void performsAggregationsWhenNaNExistsForSomeFieldValues() throws Excepti 0)); CollectionReference collection = testCollectionWithDocs(testDocs); AggregateQuerySnapshot snapshot = - collection.aggregate(sum("rating"), sum("pages"), average("year")).get().get(); + verifyPipelineReturnsSameResult( + collection.aggregate(sum("rating"), sum("pages"), average("year"))); assertThat(snapshot.get(sum("rating"))).isEqualTo(Double.NaN); assertThat(snapshot.get(sum("pages"))).isEqualTo(300); assertThat(snapshot.get(average("year"))).isEqualTo(2000); @@ -363,7 +401,8 @@ public void performsAggregationsWhenNaNExistsForSomeFieldValues() throws Excepti @Test public void throwsAnErrorWhenGettingTheResultOfAnUnrequestedAggregation() throws Exception { CollectionReference collection = testCollectionWithDocs(testDocs1); - AggregateQuerySnapshot snapshot = collection.aggregate(sum("pages")).get().get(); + AggregateQuerySnapshot snapshot = + verifyPipelineReturnsSameResult(collection.aggregate(sum("pages"))); Exception exception = null; try { snapshot.get(average("pages")); @@ -407,16 +446,15 @@ public void performsAggregationWhenUsingInOperator() throws Exception { 0)); CollectionReference collection = testCollectionWithDocs(testDocs); AggregateQuerySnapshot snapshot = - collection - .whereIn("rating", asList(5, 3)) - .aggregate( - sum("rating"), - average("rating"), - sum("pages"), - average("pages"), - AggregateField.count()) - .get() - .get(); + verifyPipelineReturnsSameResult( + collection + .whereIn("rating", asList(5, 3)) + .aggregate( + sum("rating"), + average("rating"), + sum("pages"), + average("pages"), + AggregateField.count())); assertThat(snapshot.get(sum("rating"))).isEqualTo(8); assertThat(snapshot.get(average("rating"))).isEqualTo(4); assertThat(snapshot.get(sum("pages"))).isEqualTo(200); @@ -465,16 +503,15 @@ public void performsAggregationWhenUsingArrayContainsAnyOperator() throws Except asList(0))); CollectionReference collection = testCollectionWithDocs(testDocs); AggregateQuerySnapshot snapshot = - collection - .whereArrayContainsAny("rating", asList(5, 3)) - .aggregate( - sum("rating"), - average("rating"), - sum("pages"), - average("pages"), - AggregateField.count()) - .get() - .get(); + verifyPipelineReturnsSameResult( + collection + .whereArrayContainsAny("rating", asList(5, 3)) + .aggregate( + sum("rating"), + average("rating"), + sum("pages"), + average("pages"), + AggregateField.count())); assertThat(snapshot.get(sum("rating"))).isEqualTo(0); assertThat(snapshot.get(average("rating"))).isEqualTo(null); assertThat(snapshot.get(sum("pages"))).isEqualTo(200); @@ -504,10 +541,9 @@ public void performsAggregationsOnNestedMapValues() throws Exception { map("pages", 50, "rating", map("critic", 4, "user", 4)))); CollectionReference collection = testCollectionWithDocs(testDocs); AggregateQuerySnapshot snapshot = - collection - .aggregate(sum("metadata.pages"), average("metadata.pages"), AggregateField.count()) - .get() - .get(); + verifyPipelineReturnsSameResult( + collection.aggregate( + sum("metadata.pages"), average("metadata.pages"), AggregateField.count())); assertThat(snapshot.get(sum("metadata.pages"))).isEqualTo(150); assertThat(snapshot.get(average("metadata.pages"))).isEqualTo(75); assertThat(snapshot.get(AggregateField.count())).isEqualTo(2); @@ -521,7 +557,8 @@ public void performsSumThatResultsInFloat() throws Exception { "b", map("author", "authorB", "title", "titleB", "rating", 4.5), "c", map("author", "authorC", "title", "titleC", "rating", 3)); CollectionReference collection = testCollectionWithDocs(testDocs); - AggregateQuerySnapshot snapshot = collection.aggregate(sum("rating")).get().get(); + AggregateQuerySnapshot snapshot = + verifyPipelineReturnsSameResult(collection.aggregate(sum("rating"))); Object sum = snapshot.get(sum("rating")); assertThat(sum instanceof Double).isTrue(); assertThat(sum).isEqualTo(12.5); @@ -535,7 +572,8 @@ public void performsSumOfIntsAndFloatsThatResultsInInt() throws Exception { "b", map("author", "authorB", "title", "titleB", "rating", 4.5), "c", map("author", "authorC", "title", "titleC", "rating", 3.5)); CollectionReference collection = testCollectionWithDocs(testDocs); - AggregateQuerySnapshot snapshot = collection.aggregate(sum("rating")).get().get(); + AggregateQuerySnapshot snapshot = + verifyPipelineReturnsSameResult(collection.aggregate(sum("rating"))); Object sum = snapshot.get(sum("rating")); assertThat(sum instanceof Double).isTrue(); assertThat(sum).isEqualTo(13.0); @@ -577,7 +615,8 @@ public void performsSumThatIsNegative() throws Exception { "c", map("author", "authorC", "title", "titleC", "rating", -101), "d", map("author", "authorD", "title", "titleD", "rating", -10000)); CollectionReference collection = testCollectionWithDocs(testDocs); - AggregateQuerySnapshot snapshot = collection.aggregate(sum("rating")).get().get(); + AggregateQuerySnapshot snapshot = + verifyPipelineReturnsSameResult(collection.aggregate(sum("rating"))); assertThat(snapshot.get(sum("rating"))).isEqualTo(-10101); } @@ -588,7 +627,8 @@ public void performsSumThatIsPositiveInfinity() throws Exception { "a", map("author", "authorA", "title", "titleA", "rating", Double.MAX_VALUE), "b", map("author", "authorB", "title", "titleB", "rating", Double.MAX_VALUE)); CollectionReference collection = testCollectionWithDocs(testDocs); - AggregateQuerySnapshot snapshot = collection.aggregate(sum("rating")).get().get(); + AggregateQuerySnapshot snapshot = + verifyPipelineReturnsSameResult(collection.aggregate(sum("rating"))); Object sum = snapshot.get(sum("rating")); assertThat(sum instanceof Double).isTrue(); assertThat(sum).isEqualTo(Double.POSITIVE_INFINITY); @@ -603,7 +643,8 @@ public void performsSumThatIsNegativeInfinity() throws Exception { "a", map("author", "authorA", "title", "titleA", "rating", -Double.MAX_VALUE), "b", map("author", "authorB", "title", "titleB", "rating", -Double.MAX_VALUE)); CollectionReference collection = testCollectionWithDocs(testDocs); - AggregateQuerySnapshot snapshot = collection.aggregate(sum("rating")).get().get(); + AggregateQuerySnapshot snapshot = + verifyPipelineReturnsSameResult(collection.aggregate(sum("rating"))); Object sum = snapshot.get(sum("rating")); assertThat(sum instanceof Double).isTrue(); assertThat(sum).isEqualTo(Double.NEGATIVE_INFINITY); @@ -627,7 +668,7 @@ public void performsSumThatIsValidButCouldOverflowDuringAggregation() throws Exc AggregateQuerySnapshot snapshot = collection.aggregate(sum("rating")).get().get(); Object sum = snapshot.get(sum("rating")); assertThat(sum instanceof Double).isTrue(); - assertThat(sum).isAnyOf(0, Double.NEGATIVE_INFINITY, Double.POSITIVE_INFINITY); + assertThat(sum).isAnyOf(0, 0.0, Double.NEGATIVE_INFINITY, Double.POSITIVE_INFINITY); } @Test @@ -639,7 +680,8 @@ public void performsSumThatIncludesNaN() throws Exception { "c", map("author", "authorC", "title", "titleC", "rating", Double.NaN), "d", map("author", "authorD", "title", "titleD", "rating", 0)); CollectionReference collection = testCollectionWithDocs(testDocs); - AggregateQuerySnapshot snapshot = collection.aggregate(sum("rating")).get().get(); + AggregateQuerySnapshot snapshot = + verifyPipelineReturnsSameResult(collection.aggregate(sum("rating"))); assertThat(snapshot.get(sum("rating"))).isEqualTo(Double.NaN); } @@ -647,7 +689,8 @@ public void performsSumThatIncludesNaN() throws Exception { public void performsSumOverResultSetOfZeroDocuments() throws Exception { CollectionReference collection = testCollectionWithDocs(testDocs1); AggregateQuerySnapshot snapshot = - collection.whereGreaterThan("pages", 200).aggregate(sum("pages")).get().get(); + verifyPipelineReturnsSameResult( + collection.whereGreaterThan("pages", 200).aggregate(sum("pages"))); assertThat(snapshot.get(sum("pages"))).isEqualTo(0); } @@ -661,7 +704,8 @@ public void performsSumOnlyOnNumericFields() throws Exception { "d", map("author", "authorD", "title", "titleD", "rating", 1)); CollectionReference collection = testCollectionWithDocs(testDocs); AggregateQuerySnapshot snapshot = - collection.aggregate(sum("rating"), AggregateField.count()).get().get(); + verifyPipelineReturnsSameResult( + collection.aggregate(sum("rating"), AggregateField.count())); assertThat(snapshot.get(sum("rating"))).isEqualTo(10); assertThat(snapshot.get(AggregateField.count())).isEqualTo(4); } @@ -671,7 +715,8 @@ public void performsSumOfMinIEEE754() throws Exception { Map> testDocs = map("a", map("author", "authorA", "title", "titleA", "rating", Double.MIN_VALUE)); CollectionReference collection = testCollectionWithDocs(testDocs); - AggregateQuerySnapshot snapshot = collection.aggregate(sum("rating")).get().get(); + AggregateQuerySnapshot snapshot = + verifyPipelineReturnsSameResult(collection.aggregate(sum("rating"))); assertThat(snapshot.get(sum("rating"))).isEqualTo(Double.MIN_VALUE); } @@ -683,7 +728,8 @@ public void performsAverageOfIntsThatResultsInAnInt() throws Exception { "b", map("author", "authorB", "title", "titleB", "rating", 5), "c", map("author", "authorC", "title", "titleC", "rating", 0)); CollectionReference collection = testCollectionWithDocs(testDocs); - AggregateQuerySnapshot snapshot = collection.aggregate(average("rating")).get().get(); + AggregateQuerySnapshot snapshot = + verifyPipelineReturnsSameResult(collection.aggregate(average("rating"))); assertThat(snapshot.get(average("rating"))).isEqualTo(5); assertThat(snapshot.getLong(average("rating"))).isEqualTo(5L); assertThat(snapshot.getDouble(average("rating"))).isEqualTo(5.0); @@ -696,7 +742,8 @@ public void performsAverageOfFloatsThatResultsInAnInt() throws Exception { "a", map("author", "authorA", "title", "titleA", "rating", 10.5), "b", map("author", "authorB", "title", "titleB", "rating", 9.5)); CollectionReference collection = testCollectionWithDocs(testDocs); - AggregateQuerySnapshot snapshot = collection.aggregate(average("rating")).get().get(); + AggregateQuerySnapshot snapshot = + verifyPipelineReturnsSameResult(collection.aggregate(average("rating"))); assertThat(snapshot.get(average("rating")) instanceof Double).isTrue(); assertThat(snapshot.get(average("rating"))).isEqualTo(10); assertThat(snapshot.getLong(average("rating"))).isEqualTo(10L); @@ -711,7 +758,8 @@ public void performsAverageOfFloatsAndIntsThatResultsInAnInt() throws Exception "b", map("author", "authorB", "title", "titleB", "rating", 9.5), "c", map("author", "authorC", "title", "titleC", "rating", 10.5)); CollectionReference collection = testCollectionWithDocs(testDocs); - AggregateQuerySnapshot snapshot = collection.aggregate(average("rating")).get().get(); + AggregateQuerySnapshot snapshot = + verifyPipelineReturnsSameResult(collection.aggregate(average("rating"))); assertThat(snapshot.get(average("rating"))).isEqualTo(10); assertThat(snapshot.getLong(average("rating"))).isEqualTo(10L); assertThat(snapshot.getDouble(average("rating"))).isEqualTo(10.0); @@ -725,7 +773,8 @@ public void performsAverageOfFloatsThatResultsInAFloat() throws Exception { "b", map("author", "authorB", "title", "titleB", "rating", 4.5), "c", map("author", "authorC", "title", "titleC", "rating", 3.5)); CollectionReference collection = testCollectionWithDocs(testDocs); - AggregateQuerySnapshot snapshot = collection.aggregate(average("rating")).get().get(); + AggregateQuerySnapshot snapshot = + verifyPipelineReturnsSameResult(collection.aggregate(average("rating"))); assertThat(snapshot.get(average("rating"))).isEqualTo(4.5); assertThat(snapshot.getDouble(average("rating"))).isEqualTo(4.5); assertThat(snapshot.getLong(average("rating"))).isEqualTo(4L); @@ -739,7 +788,8 @@ public void performsAverageOfFloatsAndIntsThatResultsInAFloat() throws Exception "b", map("author", "authorB", "title", "titleB", "rating", 9), "c", map("author", "authorC", "title", "titleC", "rating", 10)); CollectionReference collection = testCollectionWithDocs(testDocs); - AggregateQuerySnapshot snapshot = collection.aggregate(average("rating")).get().get(); + AggregateQuerySnapshot snapshot = + verifyPipelineReturnsSameResult(collection.aggregate(average("rating"))); assertThat(snapshot.get(average("rating"))).isEqualTo(27.6 / 3); assertThat(snapshot.getDouble(average("rating"))).isEqualTo(27.6 / 3); assertThat(snapshot.getLong(average("rating"))).isEqualTo(9L); @@ -752,7 +802,8 @@ public void performsAverageOfIntsThatResultsInAFloat() throws Exception { "a", map("author", "authorA", "title", "titleA", "rating", 10), "b", map("author", "authorB", "title", "titleB", "rating", 9)); CollectionReference collection = testCollectionWithDocs(testDocs); - AggregateQuerySnapshot snapshot = collection.aggregate(average("rating")).get().get(); + AggregateQuerySnapshot snapshot = + verifyPipelineReturnsSameResult(collection.aggregate(average("rating"))); assertThat(snapshot.get(average("rating"))).isEqualTo(9.5); assertThat(snapshot.getDouble(average("rating"))).isEqualTo(9.5d); assertThat(snapshot.getLong(average("rating"))).isEqualTo(9L); @@ -765,7 +816,8 @@ public void performsAverageCausingUnderflow() throws Exception { "a", map("author", "authorA", "title", "titleA", "rating", Double.MIN_VALUE), "b", map("author", "authorB", "title", "titleB", "rating", 0)); CollectionReference collection = testCollectionWithDocs(testDocs); - AggregateQuerySnapshot snapshot = collection.aggregate(average("rating")).get().get(); + AggregateQuerySnapshot snapshot = + verifyPipelineReturnsSameResult(collection.aggregate(average("rating"))); assertThat(snapshot.get(average("rating"))).isEqualTo(0); assertThat(snapshot.getDouble(average("rating"))).isEqualTo(0.0d); assertThat(snapshot.getLong(average("rating"))).isEqualTo(0L); @@ -776,7 +828,8 @@ public void performsAverageOfMinIEEE754() throws Exception { Map> testDocs = map("a", map("author", "authorA", "title", "titleA", "rating", Double.MIN_VALUE)); CollectionReference collection = testCollectionWithDocs(testDocs); - AggregateQuerySnapshot snapshot = collection.aggregate(average("rating")).get().get(); + AggregateQuerySnapshot snapshot = + verifyPipelineReturnsSameResult(collection.aggregate(average("rating"))); assertThat(snapshot.get(average("rating"))).isEqualTo(Double.MIN_VALUE); assertThat(snapshot.getDouble(average("rating"))).isEqualTo(Double.MIN_VALUE); assertThat(snapshot.getLong(average("rating"))).isEqualTo(0); @@ -791,7 +844,8 @@ public void performsAverageThatCouldOverflowIEEE754DuringAccumulation() throws E "b", map("author", "authorB", "title", "titleB", "rating", Double.MAX_VALUE)); CollectionReference collection = testCollectionWithDocs(testDocs); - AggregateQuerySnapshot snapshot = collection.aggregate(average("rating")).get().get(); + AggregateQuerySnapshot snapshot = + verifyPipelineReturnsSameResult(collection.aggregate(average("rating"))); assertThat(snapshot.get(average("rating"))).isEqualTo(Double.POSITIVE_INFINITY); assertThat(snapshot.getDouble(average("rating"))).isEqualTo(Double.POSITIVE_INFINITY); assertThat(snapshot.getLong(average("rating"))).isEqualTo(Long.MAX_VALUE); @@ -810,7 +864,8 @@ public void performsAverageThatIncludesNaN() throws Exception { "d", map("author", "authorD", "title", "titleD", "rating", 0)); CollectionReference collection = testCollectionWithDocs(testDocs); - AggregateQuerySnapshot snapshot = collection.aggregate(average("rating")).get().get(); + AggregateQuerySnapshot snapshot = + verifyPipelineReturnsSameResult(collection.aggregate(average("rating"))); assertThat(snapshot.get(average("rating"))).isEqualTo(Double.NaN); assertThat(snapshot.getDouble(average("rating"))).isEqualTo(Double.NaN); assertThat(snapshot.getLong(average("rating"))).isEqualTo(0L); @@ -820,7 +875,8 @@ public void performsAverageThatIncludesNaN() throws Exception { public void performsAverageOverResultSetOfZeroDocuments() throws Exception { CollectionReference collection = testCollectionWithDocs(testDocs1); AggregateQuerySnapshot snapshot = - collection.whereGreaterThan("pages", 200).aggregate(average("pages")).get().get(); + verifyPipelineReturnsSameResult( + collection.whereGreaterThan("pages", 200).aggregate(average("pages"))); assertThat(snapshot.get(average("pages"))).isEqualTo(null); assertThat(snapshot.getDouble(average("pages"))).isEqualTo(null); assertThat(snapshot.getLong(average("pages"))).isEqualTo(null); @@ -836,7 +892,8 @@ public void performsAverageOnlyOnNumericFields() throws Exception { "d", map("author", "authorD", "title", "titleD", "rating", 6)); CollectionReference collection = testCollectionWithDocs(testDocs); AggregateQuerySnapshot snapshot = - collection.aggregate(average("rating"), AggregateField.count()).get().get(); + verifyPipelineReturnsSameResult( + collection.aggregate(average("rating"), AggregateField.count())); assertThat(snapshot.get(average("rating"))).isEqualTo(5); assertThat(snapshot.get(AggregateField.count())).isEqualTo(4); } @@ -855,39 +912,35 @@ public void aggregatesWithDocumentReferenceCursors() throws Exception { CollectionReference collection = testCollectionWithDocs(testDocs); AggregateQuerySnapshot snapshot = - collection - .orderBy(FieldPath.documentId()) - .startAfter(collection.document("c")) - .aggregate(sum("num")) - .get() - .get(); + verifyPipelineReturnsSameResult( + collection + .orderBy(FieldPath.documentId()) + .startAfter(collection.document("c")) + .aggregate(sum("num"))); assertThat(snapshot.get(sum("num"))).isEqualTo(9); snapshot = - collection - .orderBy(FieldPath.documentId()) - .startAt(collection.document("c")) - .aggregate(sum("num")) - .get() - .get(); + verifyPipelineReturnsSameResult( + collection + .orderBy(FieldPath.documentId()) + .startAt(collection.document("c")) + .aggregate(sum("num"))); assertThat(snapshot.get(sum("num"))).isEqualTo(12); snapshot = - collection - .orderBy(FieldPath.documentId()) - .endBefore(collection.document("c")) - .aggregate(sum("num")) - .get() - .get(); + verifyPipelineReturnsSameResult( + collection + .orderBy(FieldPath.documentId()) + .endBefore(collection.document("c")) + .aggregate(sum("num"))); assertThat(snapshot.get(sum("num"))).isEqualTo(3); snapshot = - collection - .orderBy(FieldPath.documentId()) - .endAt(collection.document("c")) - .aggregate(sum("num")) - .get() - .get(); + verifyPipelineReturnsSameResult( + collection + .orderBy(FieldPath.documentId()) + .endAt(collection.document("c")) + .aggregate(sum("num"))); assertThat(snapshot.get(sum("num"))).isEqualTo(6); } @@ -903,7 +956,7 @@ CollectionReference addTwoDocsForCursorTesting() throws InterruptedException { public void aggregateWithNoFilterNoOrderByNoCursor() throws Exception { CollectionReference collection = addTwoDocsForCursorTesting(); AggregateQuery query = collection.aggregate(sum("num")); - AggregateQuerySnapshot snapshot = query.get().get(); + AggregateQuerySnapshot snapshot = verifyPipelineReturnsSameResult(query); assertThat(snapshot.get(sum("num"))).isEqualTo(12); } @@ -911,7 +964,7 @@ public void aggregateWithNoFilterNoOrderByNoCursor() throws Exception { public void aggregateWithEqualityFilterNoOrderByNoCursor() throws Exception { CollectionReference collection = addTwoDocsForCursorTesting(); AggregateQuery query = collection.whereEqualTo("num", 5).aggregate(sum("num")); - AggregateQuerySnapshot snapshot = query.get().get(); + AggregateQuerySnapshot snapshot = verifyPipelineReturnsSameResult(query); assertThat(snapshot.get(sum("num"))).isEqualTo(5); } @@ -919,7 +972,7 @@ public void aggregateWithEqualityFilterNoOrderByNoCursor() throws Exception { public void aggregateWithInequalityFilterNoOrderByNoCursor() throws Exception { CollectionReference collection = addTwoDocsForCursorTesting(); AggregateQuery query = collection.whereGreaterThan("num", 5).aggregate(sum("num")); - AggregateQuerySnapshot snapshot = query.get().get(); + AggregateQuerySnapshot snapshot = verifyPipelineReturnsSameResult(query); assertThat(snapshot.get(sum("num"))).isEqualTo(7); } @@ -927,7 +980,7 @@ public void aggregateWithInequalityFilterNoOrderByNoCursor() throws Exception { public void aggregateWithNoFilterExplicitOrderByNoCursor() throws Exception { CollectionReference collection = addTwoDocsForCursorTesting(); AggregateQuery query = collection.orderBy("num").aggregate(sum("num")); - AggregateQuerySnapshot snapshot = query.get().get(); + AggregateQuerySnapshot snapshot = verifyPipelineReturnsSameResult(query); assertThat(snapshot.get(sum("num"))).isEqualTo(12); } @@ -935,7 +988,7 @@ public void aggregateWithNoFilterExplicitOrderByNoCursor() throws Exception { public void aggregateWithEqualityFilterExplicitOrderByNoCursor() throws Exception { CollectionReference collection = addTwoDocsForCursorTesting(); AggregateQuery query = collection.whereEqualTo("num", 5).orderBy("num").aggregate(sum("num")); - AggregateQuerySnapshot snapshot = query.get().get(); + AggregateQuerySnapshot snapshot = verifyPipelineReturnsSameResult(query); assertThat(snapshot.get(sum("num"))).isEqualTo(5); } @@ -944,7 +997,7 @@ public void aggregateWithInequalityFilterExplicitOrderByNoCursor() throws Except CollectionReference collection = addTwoDocsForCursorTesting(); AggregateQuery query = collection.whereGreaterThan("num", 5).orderBy("num").aggregate(sum("num")); - AggregateQuerySnapshot snapshot = query.get().get(); + AggregateQuerySnapshot snapshot = verifyPipelineReturnsSameResult(query); assertThat(snapshot.get(sum("num"))).isEqualTo(7); } @@ -952,7 +1005,7 @@ public void aggregateWithInequalityFilterExplicitOrderByNoCursor() throws Except public void aggregateNoFilterExplicitOrderByFieldValueCursor() throws Exception { CollectionReference collection = addTwoDocsForCursorTesting(); AggregateQuery query = collection.orderBy("num").startAfter(5).aggregate(sum("num")); - AggregateQuerySnapshot snapshot = query.get().get(); + AggregateQuerySnapshot snapshot = verifyPipelineReturnsSameResult(query); assertThat(snapshot.get(sum("num"))).isEqualTo(7); } @@ -966,7 +1019,7 @@ public void aggregateNoFilterExplicitOrderByDocumentReferenceCursor() throws Exc .orderBy(FieldPath.documentId()) .startAfter(collection.document("a")) .aggregate(sum("num")); - AggregateQuerySnapshot snapshot = query.get().get(); + AggregateQuerySnapshot snapshot = verifyPipelineReturnsSameResult(query); assertThat(snapshot.get(sum("num"))).isEqualTo(7); } @@ -976,7 +1029,7 @@ public void aggregateNoFilterExplicitOrderByDocumentReferenceCursor() throws Exc public void aggregateNoFilterNoOrderByDocumentReferenceCursor() throws Exception { CollectionReference collection = addTwoDocsForCursorTesting(); AggregateQuery query = collection.startAfter(collection.document("a")).aggregate(sum("num")); - AggregateQuerySnapshot snapshot = query.get().get(); + AggregateQuerySnapshot snapshot = verifyPipelineReturnsSameResult(query); assertThat(snapshot.get(sum("num"))).isEqualTo(7); } @@ -987,7 +1040,7 @@ public void aggregateNoFilterExplicitOrderByDocumentSnapshotCursor() throws Exce CollectionReference collection = addTwoDocsForCursorTesting(); DocumentSnapshot docSnapshot = collection.document("a").get().get(); AggregateQuery query = collection.orderBy("foo").startAfter(docSnapshot).aggregate(sum("num")); - AggregateQuerySnapshot snapshot = query.get().get(); + AggregateQuerySnapshot snapshot = verifyPipelineReturnsSameResult(query); assertThat(snapshot.get(sum("num"))).isEqualTo(7); } @@ -997,7 +1050,7 @@ public void aggregateNoFilterExplicitOrderByDocumentSnapshotCursor2() throws Exc CollectionReference collection = addTwoDocsForCursorTesting(); DocumentSnapshot docSnapshot = collection.document("a").get().get(); AggregateQuery query = collection.orderBy("num").startAfter(docSnapshot).aggregate(sum("num")); - AggregateQuerySnapshot snapshot = query.get().get(); + AggregateQuerySnapshot snapshot = verifyPipelineReturnsSameResult(query); assertThat(snapshot.get(sum("num"))).isEqualTo(7); } @@ -1006,7 +1059,7 @@ public void aggregateEqualityFilterExplicitOrderByFieldValueCursor() throws Exce CollectionReference collection = addTwoDocsForCursorTesting(); AggregateQuery query = collection.whereEqualTo("num", 5).orderBy("num").startAt(5).aggregate(sum("num")); - AggregateQuerySnapshot snapshot = query.get().get(); + AggregateQuerySnapshot snapshot = verifyPipelineReturnsSameResult(query); assertThat(snapshot.get(sum("num"))).isEqualTo(5); } @@ -1015,7 +1068,7 @@ public void aggregateInequalityFilterExplicitOrderByFieldValueCursor() throws Ex CollectionReference collection = addTwoDocsForCursorTesting(); AggregateQuery query = collection.whereGreaterThan("num", 5).orderBy("num").startAt(6).aggregate(sum("num")); - AggregateQuerySnapshot snapshot = query.get().get(); + AggregateQuerySnapshot snapshot = verifyPipelineReturnsSameResult(query); assertThat(snapshot.get(sum("num"))).isEqualTo(7); } @@ -1030,7 +1083,7 @@ public void aggregateEqualityFilterExplicitOrderByDocumentReferenceCursor() thro .orderBy(FieldPath.documentId()) .startAfter(collection.document("a")) .aggregate(sum("num")); - AggregateQuerySnapshot snapshot = query.get().get(); + AggregateQuerySnapshot snapshot = verifyPipelineReturnsSameResult(query); assertThat(snapshot.get(sum("num"))).isEqualTo(7); } @@ -1045,7 +1098,7 @@ public void aggregateInequalityFilterExplicitOrderByDocumentReferenceCursor() th .orderBy(FieldPath.documentId()) .startAfter(5, collection.document("a")) .aggregate(sum("num")); - AggregateQuerySnapshot snapshot = query.get().get(); + AggregateQuerySnapshot snapshot = verifyPipelineReturnsSameResult(query); assertThat(snapshot.get(sum("num"))).isEqualTo(7); } @@ -1057,7 +1110,7 @@ public void aggregateEqualityFilterNoOrderByDocumentSnapshotReference() throws E DocumentSnapshot docSnapshot = collection.document("a").get().get(); AggregateQuery query = collection.whereEqualTo("num", 7).startAfter(docSnapshot).aggregate(sum("num")); - AggregateQuerySnapshot snapshot = query.get().get(); + AggregateQuerySnapshot snapshot = verifyPipelineReturnsSameResult(query); assertThat(snapshot.get(sum("num"))).isEqualTo(7); } @@ -1068,7 +1121,7 @@ public void aggregateInequalityFilterNoOrderByDocumentSnapshotReference() throws DocumentSnapshot docSnapshot = collection.document("a").get().get(); AggregateQuery query = collection.whereGreaterThan("num", 0).startAfter(docSnapshot).aggregate(sum("num")); - AggregateQuerySnapshot snapshot = query.get().get(); + AggregateQuerySnapshot snapshot = verifyPipelineReturnsSameResult(query); assertThat(snapshot.get(sum("num"))).isEqualTo(7); } @@ -1080,7 +1133,7 @@ public void aggregateInequalityFilterNoOrderByDocumentSnapshotReference2() throw DocumentSnapshot docSnapshot = collection.document("a").get().get(); AggregateQuery query = collection.whereGreaterThan("foo", 0).startAfter(docSnapshot).aggregate(sum("num")); - AggregateQuerySnapshot snapshot = query.get().get(); + AggregateQuerySnapshot snapshot = verifyPipelineReturnsSameResult(query); assertThat(snapshot.get(sum("num"))).isEqualTo(7); } diff --git a/google-cloud-firestore/src/test/java/com/google/cloud/firestore/it/ITQueryTest.java b/google-cloud-firestore/src/test/java/com/google/cloud/firestore/it/ITQueryTest.java index 14822b4b0..9bf0085ca 100644 --- a/google-cloud-firestore/src/test/java/com/google/cloud/firestore/it/ITQueryTest.java +++ b/google-cloud-firestore/src/test/java/com/google/cloud/firestore/it/ITQueryTest.java @@ -20,27 +20,24 @@ import static com.google.common.primitives.Ints.asList; import static com.google.common.truth.Truth.assertThat; import static org.junit.Assert.assertEquals; -import static org.junit.Assert.fail; import static org.junit.Assume.assumeTrue; -import com.google.api.core.ApiFuture; -import com.google.api.gax.rpc.ApiStreamObserver; import com.google.cloud.firestore.*; import com.google.cloud.firestore.Query.Direction; -import java.time.Duration; +import com.google.common.collect.Sets; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; -import java.util.Iterator; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; +import java.util.Objects; +import java.util.Set; import java.util.concurrent.CountDownLatch; import java.util.concurrent.ExecutionException; import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeoutException; import java.util.stream.Collectors; -import javax.annotation.Nullable; import org.junit.Rule; import org.junit.Test; import org.junit.rules.TestName; @@ -75,7 +72,13 @@ public CollectionReference testCollectionWithDocs(Map result = @@ -83,6 +86,42 @@ public static void checkQuerySnapshotContainsDocuments(Query query, String... do .map(queryDocumentSnapshot -> queryDocumentSnapshot.getReference().getId()) .collect(Collectors.toList()); assertThat(result).isEqualTo(Arrays.asList(docs)); + + if (edition == FirestoreEdition.ENTERPRISE) { + List pipelineResults = + query.getFirestore().pipeline().createFrom(query).execute().get().getResults(); + result = + pipelineResults.stream() + .map(pipelineResult -> Objects.requireNonNull(pipelineResult.getReference()).getId()) + .collect(Collectors.toList()); + assertThat(result).isEqualTo(Arrays.asList(docs)); + } + } + + public static void checkResultContainsDocuments(Query query, String... docs) + throws ExecutionException, InterruptedException { + checkResultContainsDocuments(query, getFirestoreEdition(), docs); + } + + public static void checkResultContainsDocuments( + Query query, FirestoreEdition edition, String... docs) + throws ExecutionException, InterruptedException { + QuerySnapshot snapshot = query.get().get(); + Set result = + snapshot.getDocuments().stream() + .map(queryDocumentSnapshot -> queryDocumentSnapshot.getReference().getId()) + .collect(Collectors.toSet()); + assertThat(result).isEqualTo(Sets.newHashSet(docs)); + + if (edition == FirestoreEdition.ENTERPRISE) { + List pipelineResults = + query.getFirestore().pipeline().createFrom(query).execute().get().getResults(); + result = + pipelineResults.stream() + .map(pipelineResult -> Objects.requireNonNull(pipelineResult.getReference()).getId()) + .collect(Collectors.toSet()); + assertThat(result).isEqualTo(Sets.newHashSet(docs)); + } } @Test @@ -98,7 +137,7 @@ public void orQueries() throws Exception { CollectionReference collection = testCollectionWithDocs(testDocs); // Two equalities: a==1 || b==1. - checkQuerySnapshotContainsDocuments( + checkResultContainsDocumentsInOrder( collection.where(Filter.or(Filter.equalTo("a", 1), Filter.equalTo("b", 1))), "doc1", "doc2", @@ -106,7 +145,7 @@ public void orQueries() throws Exception { "doc5"); // (a==1 && b==0) || (a==3 && b==2) - checkQuerySnapshotContainsDocuments( + checkResultContainsDocumentsInOrder( collection.where( Filter.or( Filter.and(Filter.equalTo("a", 1), Filter.equalTo("b", 0)), @@ -115,7 +154,7 @@ public void orQueries() throws Exception { "doc3"); // a==1 && (b==0 || b==3). - checkQuerySnapshotContainsDocuments( + checkResultContainsDocumentsInOrder( collection.where( Filter.and( Filter.equalTo("a", 1), Filter.or(Filter.equalTo("b", 0), Filter.equalTo("b", 3)))), @@ -123,7 +162,7 @@ public void orQueries() throws Exception { "doc4"); // (a==2 || b==2) && (a==3 || b==3) - checkQuerySnapshotContainsDocuments( + checkResultContainsDocumentsInOrder( collection.where( Filter.and( Filter.or(Filter.equalTo("a", 2), Filter.equalTo("b", 2)), @@ -131,7 +170,7 @@ public void orQueries() throws Exception { "doc3"); // Test with limits without orderBy (the __name__ ordering is the tiebreaker). - checkQuerySnapshotContainsDocuments( + checkResultContainsDocumentsInOrder( collection.where(Filter.or(Filter.equalTo("a", 2), Filter.equalTo("b", 1))).limit(1), "doc2"); } @@ -153,21 +192,21 @@ public void orQueriesWithCompositeIndexes() throws Exception { CollectionReference collection = testCollectionWithDocs(testDocs); // with one inequality: a>2 || b==1. - checkQuerySnapshotContainsDocuments( + checkResultContainsDocuments( collection.where(Filter.or(Filter.greaterThan("a", 2), Filter.equalTo("b", 1))), "doc5", "doc2", "doc3"); // Test with limits (implicit order by ASC): (a==1) || (b > 0) LIMIT 2 - checkQuerySnapshotContainsDocuments( + checkResultContainsDocumentsInOrder( collection.where(Filter.or(Filter.equalTo("a", 1), Filter.greaterThan("b", 0))).limit(2), "doc1", "doc2"); // Test with limits (explicit order by): (a==1) || (b > 0) LIMIT_TO_LAST 2 // Note: The public query API does not allow implicit ordering when limitToLast is used. - checkQuerySnapshotContainsDocuments( + checkResultContainsDocumentsInOrder( collection .where(Filter.or(Filter.equalTo("a", 1), Filter.greaterThan("b", 0))) .limitToLast(2) @@ -176,7 +215,7 @@ public void orQueriesWithCompositeIndexes() throws Exception { "doc4"); // Test with limits (explicit order by ASC): (a==2) || (b == 1) ORDER BY a LIMIT 1 - checkQuerySnapshotContainsDocuments( + checkResultContainsDocumentsInOrder( collection .where(Filter.or(Filter.equalTo("a", 2), Filter.equalTo("b", 1))) .limit(1) @@ -184,7 +223,7 @@ public void orQueriesWithCompositeIndexes() throws Exception { "doc5"); // Test with limits (explicit order by DESC): (a==2) || (b == 1) ORDER BY a LIMIT_TO_LAST 1 - checkQuerySnapshotContainsDocuments( + checkResultContainsDocumentsInOrder( collection .where(Filter.or(Filter.equalTo("a", 2), Filter.equalTo("b", 1))) .limitToLast(1) @@ -192,7 +231,7 @@ public void orQueriesWithCompositeIndexes() throws Exception { "doc2"); // Test with limits (explicit order by DESC): (a==2) || (b == 1) ORDER BY a DESC LIMIT 1 - checkQuerySnapshotContainsDocuments( + checkResultContainsDocumentsInOrder( collection .where(Filter.or(Filter.equalTo("a", 2), Filter.equalTo("b", 1))) .limit(1) @@ -217,7 +256,7 @@ public void orQueryDoesNotIncludeDocumentsWithMissingFields() throws Exception { // There's no explicit nor implicit orderBy. Documents with missing 'a' or missing 'b' should be // allowed if the document matches at least one disjunction term. Query query = collection.where(Filter.or(Filter.equalTo("a", 1), Filter.equalTo("b", 1))); - checkQuerySnapshotContainsDocuments(query, "doc1", "doc2", "doc4", "doc5"); + checkResultContainsDocumentsInOrder(query, "doc1", "doc2", "doc4", "doc5"); } @Test @@ -241,19 +280,19 @@ public void orQueryDoesNotIncludeDocumentsWithMissingFields2() throws Exception // doc2 should not be included because it's missing the field 'a', and we have "orderBy a". Query query1 = collection.where(Filter.or(Filter.equalTo("a", 1), Filter.equalTo("b", 1))).orderBy("a"); - checkQuerySnapshotContainsDocuments(query1, "doc1", "doc4", "doc5"); + checkResultContainsDocumentsInOrder(query1, "doc1", "doc4", "doc5"); // Query: a==1 || b==1 order by b. // doc5 should not be included because it's missing the field 'b', and we have "orderBy b". Query query2 = collection.where(Filter.or(Filter.equalTo("a", 1), Filter.equalTo("b", 1))).orderBy("b"); - checkQuerySnapshotContainsDocuments(query2, "doc1", "doc2", "doc4"); + checkResultContainsDocumentsInOrder(query2, "doc1", "doc2", "doc4"); // Query: a>2 || b==1. // This query has an implicit 'order by a'. // doc2 should not be included because it's missing the field 'a'. Query query3 = collection.where(Filter.or(Filter.greaterThan("a", 2), Filter.equalTo("b", 1))); - checkQuerySnapshotContainsDocuments(query3, "doc3"); + checkResultContainsDocumentsInOrder(query3, "doc3"); // Query: a>1 || b==1 order by a order by b. // doc6 should not be included because it's missing the field 'b'. @@ -263,7 +302,7 @@ public void orQueryDoesNotIncludeDocumentsWithMissingFields2() throws Exception .where(Filter.or(Filter.greaterThan("a", 1), Filter.equalTo("b", 1))) .orderBy("a") .orderBy("b"); - checkQuerySnapshotContainsDocuments(query4, "doc3"); + checkResultContainsDocumentsInOrder(query4, "doc3"); } @Test @@ -279,7 +318,7 @@ public void orQueriesWithIn() throws ExecutionException, InterruptedException, T CollectionReference collection = testCollectionWithDocs(testDocs); // a==2 || b in [2,3] - checkQuerySnapshotContainsDocuments( + checkResultContainsDocumentsInOrder( collection.where(Filter.or(Filter.equalTo("a", 2), Filter.inArray("b", asList(2, 3)))), "doc3", "doc4", @@ -290,7 +329,8 @@ public void orQueriesWithIn() throws ExecutionException, InterruptedException, T public void orQueriesWithNotIn() throws ExecutionException, InterruptedException, TimeoutException { assumeTrue( - "Skip this test when running against production because it is currently not supported.", + "Skip this test when running against production because it requires composite index" + + " creation.", isRunningAgainstFirestoreEmulator(firestore)); Map> testDocs = map( @@ -304,7 +344,7 @@ public void orQueriesWithNotIn() // a==2 || b not-in [2,3] // Has implicit orderBy b. - checkQuerySnapshotContainsDocuments( + checkResultContainsDocumentsInOrder( collection.where(Filter.or(Filter.equalTo("a", 2), Filter.notInArray("b", asList(2, 3)))), "doc1", "doc2"); @@ -324,14 +364,14 @@ public void orQueriesWithArrayMembership() CollectionReference collection = testCollectionWithDocs(testDocs); // a==2 || b array-contains 7 - checkQuerySnapshotContainsDocuments( + checkResultContainsDocumentsInOrder( collection.where(Filter.or(Filter.equalTo("a", 2), Filter.arrayContains("b", 7))), "doc3", "doc4", "doc6"); // a==2 || b array-contains-any [0, 3] - checkQuerySnapshotContainsDocuments( + checkResultContainsDocumentsInOrder( collection.where( Filter.or(Filter.equalTo("a", 2), Filter.arrayContainsAny("b", asList(0, 3)))), "doc1", @@ -355,35 +395,31 @@ public void testUsingInWithArrayContains() Query query1 = collection.where( Filter.or(Filter.inArray("a", asList(2, 3)), Filter.arrayContains("b", 3))); - checkQuerySnapshotContainsDocuments(query1, "doc3", "doc4", "doc6"); + checkResultContainsDocumentsInOrder(query1, "doc3", "doc4", "doc6"); Query query2 = collection.where( Filter.and(Filter.inArray("a", asList(2, 3)), Filter.arrayContains("b", 7))); - checkQuerySnapshotContainsDocuments(query2, "doc3"); + checkResultContainsDocumentsInOrder(query2, "doc3"); Query query3 = collection.where( Filter.or( Filter.inArray("a", asList(2, 3)), Filter.and(Filter.arrayContains("b", 3), Filter.equalTo("a", 1)))); - checkQuerySnapshotContainsDocuments(query3, "doc3", "doc4", "doc6"); + checkResultContainsDocumentsInOrder(query3, "doc3", "doc4", "doc6"); Query query4 = collection.where( Filter.and( Filter.inArray("a", asList(2, 3)), Filter.or(Filter.arrayContains("b", 7), Filter.equalTo("a", 1)))); - checkQuerySnapshotContainsDocuments(query4, "doc3"); + checkResultContainsDocumentsInOrder(query4, "doc3"); } @Test public void testOrderByEquality() throws ExecutionException, InterruptedException, TimeoutException { - assumeTrue( - "Skip this test if running against production because order-by-equality is " - + "not supported yet.", - isRunningAgainstFirestoreEmulator(firestore)); Map> testDocs = map( "doc1", map("a", 1, "b", asList(0)), @@ -395,20 +431,18 @@ public void testOrderByEquality() CollectionReference collection = testCollectionWithDocs(testDocs); Query query1 = collection.where(Filter.equalTo("a", 1)).orderBy("a"); - checkQuerySnapshotContainsDocuments(query1, "doc1", "doc4", "doc5"); + checkResultContainsDocumentsInOrder(query1, "doc1", "doc4", "doc5"); Query query2 = collection.where(Filter.inArray("a", asList(2, 3))).orderBy("a"); - checkQuerySnapshotContainsDocuments(query2, "doc6", "doc3"); + checkResultContainsDocumentsInOrder(query2, "doc6", "doc3"); } /** Multiple Inequality */ @Test public void multipleInequalityOnDifferentFields() throws Exception { - // TODO(MIEQ): Enable this test against production when possible. assumeTrue( - "Skip this test if running against production because multiple inequality is " - + "not supported yet.", - isRunningAgainstFirestoreEmulator(firestore)); + "Standard edition requires index setup, but this is a dynamic collection", + getFirestoreEdition() == FirestoreEdition.ENTERPRISE); CollectionReference collection = testCollectionWithDocs( @@ -423,7 +457,7 @@ public void multipleInequalityOnDifferentFields() throws Exception { .whereNotEqualTo("key", "a") .whereLessThanOrEqualTo("sort", 2) .whereGreaterThan("v", 2); - checkQuerySnapshotContainsDocuments(query1, "doc3"); + checkResultContainsDocumentsInOrder(query1, "doc3"); // Duplicate inequality fields Query query2 = @@ -431,7 +465,7 @@ public void multipleInequalityOnDifferentFields() throws Exception { .whereNotEqualTo("key", "a") .whereLessThanOrEqualTo("sort", 2) .whereGreaterThan("sort", 1); - checkQuerySnapshotContainsDocuments(query2, "doc4"); + checkResultContainsDocumentsInOrder(query2, "doc4"); // With multiple IN Query query3 = @@ -440,7 +474,7 @@ public void multipleInequalityOnDifferentFields() throws Exception { .whereLessThanOrEqualTo("sort", 2) .whereIn("v", asList(2, 3, 4)) .whereIn("sort", asList(2, 3)); - checkQuerySnapshotContainsDocuments(query3, "doc4"); + checkResultContainsDocumentsInOrder(query3, "doc4"); // With NOT-IN Query query4 = @@ -448,7 +482,7 @@ public void multipleInequalityOnDifferentFields() throws Exception { .whereGreaterThanOrEqualTo("key", "a") .whereLessThanOrEqualTo("sort", 2) .whereNotIn("v", asList(2, 4, 5)); - checkQuerySnapshotContainsDocuments(query4, "doc1", "doc3"); + checkResultContainsDocumentsInOrder(query4, "doc1", "doc3"); // With orderby Query query5 = @@ -456,7 +490,7 @@ public void multipleInequalityOnDifferentFields() throws Exception { .whereGreaterThanOrEqualTo("key", "a") .whereLessThanOrEqualTo("sort", 2) .orderBy("v", Direction.DESCENDING); - checkQuerySnapshotContainsDocuments(query5, "doc3", "doc4", "doc1"); + checkResultContainsDocumentsInOrder(query5, "doc3", "doc4", "doc1"); // With limit Query query6 = @@ -465,7 +499,7 @@ public void multipleInequalityOnDifferentFields() throws Exception { .whereLessThanOrEqualTo("sort", 2) .orderBy("v", Direction.DESCENDING) .limit(2); - checkQuerySnapshotContainsDocuments(query6, "doc3", "doc4"); + checkResultContainsDocumentsInOrder(query6, "doc3", "doc4"); // With limitToLast Query query7 = @@ -474,17 +508,14 @@ public void multipleInequalityOnDifferentFields() throws Exception { .whereLessThanOrEqualTo("sort", 2) .orderBy("v", Direction.DESCENDING) .limitToLast(2); - checkQuerySnapshotContainsDocuments(query7, "doc4", "doc1"); + checkResultContainsDocumentsInOrder(query7, "doc4", "doc1"); } @Test public void multipleInequalityOnSpecialValues() throws Exception { - // TODO(MIEQ): Enable this test against production when possible. assumeTrue( - "Skip this test if running against production because multiple inequality is " - + "not supported yet.", - isRunningAgainstFirestoreEmulator(firestore)); - + "Standard edition requires index setup, but this is a dynamic collection", + getFirestoreEdition() == FirestoreEdition.ENTERPRISE); CollectionReference collection = testCollectionWithDocs( map( @@ -496,23 +527,21 @@ public void multipleInequalityOnSpecialValues() throws Exception { "doc6", map("key", "f", "sort", 1, "v", 1))); Query query1 = collection.whereNotEqualTo("key", "a").whereLessThanOrEqualTo("sort", 2); - checkQuerySnapshotContainsDocuments(query1, "doc5", "doc6"); + checkResultContainsDocumentsInOrder(query1, "doc5", "doc6"); Query query2 = collection .whereNotEqualTo("key", "a") .whereLessThanOrEqualTo("sort", 2) .whereLessThanOrEqualTo("v", 1); - checkQuerySnapshotContainsDocuments(query2, "doc6"); + checkResultContainsDocumentsInOrder(query2, "doc6"); } @Test public void multipleInequalityWithArrayMembership() throws Exception { - // TODO(MIEQ): Enable this test against production when possible. assumeTrue( - "Skip this test if running against production because multiple inequality is " - + "not supported yet.", - isRunningAgainstFirestoreEmulator(firestore)); + "Standard edition requires index setup, but this is a dynamic collection", + getFirestoreEdition() == FirestoreEdition.ENTERPRISE); CollectionReference collection = testCollectionWithDocs( @@ -537,14 +566,14 @@ public void multipleInequalityWithArrayMembership() throws Exception { .whereNotEqualTo("key", "a") .whereGreaterThanOrEqualTo("sort", 1) .whereArrayContains("v", 0); - checkQuerySnapshotContainsDocuments(query1, "doc2"); + checkResultContainsDocumentsInOrder(query1, "doc2"); Query query2 = collection .whereNotEqualTo("key", "a") .whereGreaterThanOrEqualTo("sort", 1) .whereArrayContainsAny("v", asList(0, 1)); - checkQuerySnapshotContainsDocuments(query2, "doc2", "doc4"); + checkResultContainsDocumentsInOrder(query2, "doc2", "doc4"); } private static Map nestedObject(int number) { @@ -565,11 +594,9 @@ private static Map nestedObject(int number) { // result with the query fields normalized in the server. @Test public void multipleInequalityWithNestedField() throws Exception { - // TODO(MIEQ): Enable this test against production when possible. assumeTrue( - "Skip this test if running against production because multiple inequality is " - + "not supported yet.", - isRunningAgainstFirestoreEmulator(firestore)); + "Standard edition requires index setup, but this is a dynamic collection", + getFirestoreEdition() == FirestoreEdition.ENTERPRISE); CollectionReference collection = testCollectionWithDocs( @@ -588,8 +615,8 @@ public void multipleInequalityWithNestedField() throws Exception { .orderBy("name"); DocumentSnapshot docSnap = collection.document("doc4").get().get(); Query query1WithCursor = query1.startAt(docSnap); - checkQuerySnapshotContainsDocuments(query1, "doc4", "doc1"); - checkQuerySnapshotContainsDocuments(query1WithCursor, "doc4", "doc1"); + checkResultContainsDocumentsInOrder(query1, "doc4", "doc1"); + checkResultContainsDocumentsInOrder(query1WithCursor, "doc4", "doc1"); // ordered by: name desc, field desc, field.dot desc, field\\slash desc, __name__ desc Query query2 = @@ -600,17 +627,15 @@ public void multipleInequalityWithNestedField() throws Exception { .orderBy("name", Direction.DESCENDING); docSnap = collection.document("doc2").get().get(); Query query2WithCursor = query2.startAt(docSnap); - checkQuerySnapshotContainsDocuments(query2, "doc2", "doc3"); - checkQuerySnapshotContainsDocuments(query2WithCursor, "doc2", "doc3"); + checkResultContainsDocumentsInOrder(query2, "doc2", "doc3"); + checkResultContainsDocumentsInOrder(query2WithCursor, "doc2", "doc3"); } @Test public void multipleInequalityWithCompositeFilters() throws Exception { - // TODO(MIEQ): Enable this test against production when possible. assumeTrue( - "Skip this test if running against production because multiple inequality is " - + "not supported yet.", - isRunningAgainstFirestoreEmulator(firestore)); + "Standard edition requires index setup, but this is a dynamic collection", + getFirestoreEdition() == FirestoreEdition.ENTERPRISE); CollectionReference collection = testCollectionWithDocs( @@ -636,8 +661,8 @@ public void multipleInequalityWithCompositeFilters() throws Exception { Filter.and(Filter.notEqualTo("key", "b"), Filter.greaterThan("v", 4)))); DocumentSnapshot docSnap = collection.document("doc1").get().get(); Query query1WithCursor = query1.startAt(docSnap); - checkQuerySnapshotContainsDocuments(query1, "doc1", "doc6", "doc5", "doc4"); - checkQuerySnapshotContainsDocuments(query1WithCursor, "doc1", "doc6", "doc5", "doc4"); + checkResultContainsDocumentsInOrder(query1, "doc1", "doc6", "doc5", "doc4"); + checkResultContainsDocumentsInOrder(query1WithCursor, "doc1", "doc6", "doc5", "doc4"); // Ordered by: 'sort' desc, 'key' asc, 'v' asc, __name__ asc Query query2 = @@ -650,8 +675,8 @@ public void multipleInequalityWithCompositeFilters() throws Exception { .orderBy("key"); docSnap = collection.document("doc5").get().get(); Query query2WithCursor = query2.startAt(docSnap); - checkQuerySnapshotContainsDocuments(query2, "doc5", "doc4", "doc1", "doc6"); - checkQuerySnapshotContainsDocuments(query2WithCursor, "doc5", "doc4", "doc1", "doc6"); + checkResultContainsDocumentsInOrder(query2, "doc5", "doc4", "doc1", "doc6"); + checkResultContainsDocumentsInOrder(query2WithCursor, "doc5", "doc4", "doc1", "doc6"); // Implicitly ordered by: 'key' asc, 'sort' asc, 'v' asc, __name__ asc Query query3 = @@ -666,19 +691,16 @@ public void multipleInequalityWithCompositeFilters() throws Exception { Filter.and(Filter.lessThan("key", "b"), Filter.greaterThan("v", 0))))); docSnap = collection.document("doc1").get().get(); Query query3WithCursor = query3.startAt(docSnap); - checkQuerySnapshotContainsDocuments(query3, "doc1", "doc2"); - checkQuerySnapshotContainsDocuments(query3WithCursor, "doc1", "doc2"); + checkResultContainsDocumentsInOrder(query3, "doc1", "doc2"); + checkResultContainsDocumentsInOrder(query3WithCursor, "doc1", "doc2"); } @Test public void multipleInequalityFieldsWillBeImplicitlyOrderedLexicographicallyByServer() throws Exception { - // TODO(MIEQ): Enable this test against production when possible. assumeTrue( - "Skip this test if running against production because multiple inequality is " - + "not supported yet.", - isRunningAgainstFirestoreEmulator(firestore)); - + "Standard edition requires index setup, but this is a dynamic collection", + getFirestoreEdition() == FirestoreEdition.ENTERPRISE); CollectionReference collection = testCollectionWithDocs( map( @@ -704,8 +726,8 @@ public void multipleInequalityFieldsWillBeImplicitlyOrderedLexicographicallyBySe .whereGreaterThan("sort", 1) .whereIn("v", asList(1, 2, 3, 4)); Query query1WithCursor = query1.startAt(docSnap); - checkQuerySnapshotContainsDocuments(query1, "doc2", "doc4", "doc5", "doc3"); - checkQuerySnapshotContainsDocuments(query1WithCursor, "doc2", "doc4", "doc5", "doc3"); + checkResultContainsDocumentsInOrder(query1, "doc2", "doc4", "doc5", "doc3"); + checkResultContainsDocumentsInOrder(query1WithCursor, "doc2", "doc4", "doc5", "doc3"); // Implicitly ordered by: 'key' asc, 'sort' asc, __name__ asc Query query2 = @@ -714,17 +736,15 @@ public void multipleInequalityFieldsWillBeImplicitlyOrderedLexicographicallyBySe .whereNotEqualTo("key", "a") .whereIn("v", asList(1, 2, 3, 4)); Query query2WithCursor = query2.startAt(docSnap); - checkQuerySnapshotContainsDocuments(query2, "doc2", "doc4", "doc5", "doc3"); - checkQuerySnapshotContainsDocuments(query2WithCursor, "doc2", "doc4", "doc5", "doc3"); + checkResultContainsDocumentsInOrder(query2, "doc2", "doc4", "doc5", "doc3"); + checkResultContainsDocumentsInOrder(query2WithCursor, "doc2", "doc4", "doc5", "doc3"); } @Test public void multipleInequalityWithMultipleExplicitOrderBy() throws Exception { - // TODO(MIEQ): Enable this test against production when possible. assumeTrue( - "Skip this test if running against production because multiple inequality is " - + "not supported yet.", - isRunningAgainstFirestoreEmulator(firestore)); + "Standard edition requires index setup, but this is a dynamic collection", + getFirestoreEdition() == FirestoreEdition.ENTERPRISE); CollectionReference collection = testCollectionWithDocs( @@ -748,8 +768,8 @@ public void multipleInequalityWithMultipleExplicitOrderBy() throws Exception { Query query1 = collection.whereGreaterThan("key", "a").whereGreaterThanOrEqualTo("sort", 1).orderBy("v"); Query query1WithCursor = query1.startAt(docSnap); - checkQuerySnapshotContainsDocuments(query1, "doc2", "doc4", "doc3", "doc5"); - checkQuerySnapshotContainsDocuments(query1WithCursor, "doc2", "doc4", "doc3", "doc5"); + checkResultContainsDocumentsInOrder(query1, "doc2", "doc4", "doc3", "doc5"); + checkResultContainsDocumentsInOrder(query1WithCursor, "doc2", "doc4", "doc3", "doc5"); // Ordered by: 'v asc, 'sort' asc, 'key' asc, __name__ asc Query query2 = @@ -759,8 +779,8 @@ public void multipleInequalityWithMultipleExplicitOrderBy() throws Exception { .orderBy("v") .orderBy("sort"); Query query2WithCursor = query2.startAt(docSnap); - checkQuerySnapshotContainsDocuments(query2, "doc2", "doc5", "doc4", "doc3"); - checkQuerySnapshotContainsDocuments(query2WithCursor, "doc2", "doc5", "doc4", "doc3"); + checkResultContainsDocumentsInOrder(query2, "doc2", "doc5", "doc4", "doc3"); + checkResultContainsDocumentsInOrder(query2WithCursor, "doc2", "doc5", "doc4", "doc3"); docSnap = collection.document("doc5").get().get(); @@ -772,8 +792,8 @@ public void multipleInequalityWithMultipleExplicitOrderBy() throws Exception { .whereGreaterThanOrEqualTo("sort", 1) .orderBy("v", Direction.DESCENDING); Query query3WithCursor = query3.startAt(docSnap); - checkQuerySnapshotContainsDocuments(query3, "doc5", "doc3", "doc4", "doc2"); - checkQuerySnapshotContainsDocuments(query3WithCursor, "doc5", "doc3", "doc4", "doc2"); + checkResultContainsDocumentsInOrder(query3, "doc5", "doc3", "doc4", "doc2"); + checkResultContainsDocumentsInOrder(query3WithCursor, "doc5", "doc3", "doc4", "doc2"); // Ordered by: 'v desc, 'sort' asc, 'key' asc, __name__ asc Query query4 = @@ -783,18 +803,12 @@ public void multipleInequalityWithMultipleExplicitOrderBy() throws Exception { .orderBy("v", Direction.DESCENDING) .orderBy("sort"); Query query4WithCursor = query4.startAt(docSnap); - checkQuerySnapshotContainsDocuments(query4, "doc5", "doc4", "doc3", "doc2"); - checkQuerySnapshotContainsDocuments(query4WithCursor, "doc5", "doc4", "doc3", "doc2"); + checkResultContainsDocumentsInOrder(query4, "doc5", "doc4", "doc3", "doc2"); + checkResultContainsDocumentsInOrder(query4WithCursor, "doc5", "doc4", "doc3", "doc2"); } @Test public void multipleInequalityFieldsInAggregateQuery() throws Exception { - // TODO(MIEQ): Enable this test against production when possible. - assumeTrue( - "Skip this test if running against production because multiple inequality is " - + "not supported yet.", - isRunningAgainstFirestoreEmulator(firestore)); - CollectionReference collection = testCollectionWithDocs( map( @@ -815,17 +829,30 @@ public void multipleInequalityFieldsInAggregateQuery() throws Exception { .whereGreaterThanOrEqualTo("sort", 1) .orderBy("v") .count(); - assertThat(query.get().get().getCount()).isEqualTo(4); + if (isRunningAgainstFirestoreEmulator(firestore)) { + assertThat(query.get().get().getCount()).isEqualTo(4); + } + + if (getFirestoreEdition() == FirestoreEdition.ENTERPRISE) { + assertThat( + query + .getQuery() + .getFirestore() + .pipeline() + .createFrom(query) + .execute() + .get() + .getResults()) + .isNotEmpty(); + } // TODO(MIEQ): Add sum and average when they are public. } @Test public void multipleInequalityFieldsWithDocumentKey() throws Exception { - // TODO(MIEQ): Enable this test against production when possible. assumeTrue( - "Skip this test if running against production because multiple inequality is " - + "not supported yet.", - isRunningAgainstFirestoreEmulator(firestore)); + "Standard edition requires index setup, but this is a dynamic collection", + getFirestoreEdition() == FirestoreEdition.ENTERPRISE); CollectionReference collection = testCollectionWithDocs( @@ -851,8 +878,8 @@ public void multipleInequalityFieldsWithDocumentKey() throws Exception { .whereNotEqualTo("key", "a") .whereLessThan(FieldPath.documentId(), "doc5"); Query query1WithCursor = query1.startAt(docSnap); - checkQuerySnapshotContainsDocuments(query1, "doc2", "doc4", "doc3"); - checkQuerySnapshotContainsDocuments(query1WithCursor, "doc2", "doc4", "doc3"); + checkResultContainsDocumentsInOrder(query1, "doc2", "doc4", "doc3"); + checkResultContainsDocumentsInOrder(query1WithCursor, "doc2", "doc4", "doc3"); // Changing filters order will not affect implicit order. // Implicitly ordered by: 'key' asc, 'sort' asc, __name__ asc @@ -862,8 +889,8 @@ public void multipleInequalityFieldsWithDocumentKey() throws Exception { .whereGreaterThan("sort", 1) .whereNotEqualTo("key", "a"); Query query2WithCursor = query2.startAt(docSnap); - checkQuerySnapshotContainsDocuments(query2, "doc2", "doc4", "doc3"); - checkQuerySnapshotContainsDocuments(query2WithCursor, "doc2", "doc4", "doc3"); + checkResultContainsDocumentsInOrder(query2, "doc2", "doc4", "doc3"); + checkResultContainsDocumentsInOrder(query2WithCursor, "doc2", "doc4", "doc3"); // Ordered by: 'sort' desc, 'key' desc, __name__ desc Query query3 = @@ -873,249 +900,8 @@ public void multipleInequalityFieldsWithDocumentKey() throws Exception { .whereNotEqualTo("key", "a") .orderBy("sort", Direction.DESCENDING); Query query3WithCursor = query3.startAt(docSnap); - checkQuerySnapshotContainsDocuments(query3, "doc2", "doc3", "doc4"); - checkQuerySnapshotContainsDocuments(query3WithCursor, "doc2", "doc3", "doc4"); - } - - @Test - public void testQueryPlan() throws Exception { - Map> testDocs = - map( - "doc1", map("a", 1, "b", asList(0)), - "doc2", map("b", asList(1)), - "doc3", map("a", 3, "b", asList(2, 7), "c", 10), - "doc4", map("a", 1, "b", asList(3, 7)), - "doc5", map("a", 1), - "doc6", map("a", 2, "c", 20)); - CollectionReference collection = testCollectionWithDocs(testDocs); - - Query query = collection.where(Filter.equalTo("a", 1)).orderBy("a"); - ExplainResults explainResults = - query.explain(ExplainOptions.builder().setAnalyze(false).build()).get(); - - @Nullable QuerySnapshot snapshot = explainResults.getSnapshot(); - assertThat(snapshot).isNull(); - - ExplainMetrics metrics = explainResults.getMetrics(); - assertThat(metrics).isNotNull(); - - PlanSummary planSummary = metrics.getPlanSummary(); - assertThat(planSummary).isNotNull(); - assertThat(planSummary.getIndexesUsed()).isNotEmpty(); - - ExecutionStats stats = metrics.getExecutionStats(); - assertThat(stats).isNull(); - } - - @Test - public void testQueryProfile() throws Exception { - Map> testDocs = - map( - "doc1", map("a", 1, "b", asList(0)), - "doc2", map("b", asList(1)), - "doc3", map("a", 3, "b", asList(2, 7), "c", 10), - "doc4", map("a", 1, "b", asList(3, 7)), - "doc5", map("a", 1), - "doc6", map("a", 2, "c", 20)); - CollectionReference collection = testCollectionWithDocs(testDocs); - - Query query = collection.where(Filter.equalTo("a", 1)).orderBy("a"); - ExplainResults explainResults = - query.explain(ExplainOptions.builder().setAnalyze(true).build()).get(); - - @Nullable QuerySnapshot snapshot = explainResults.getSnapshot(); - assertThat(snapshot).isNotNull(); - assertThat(snapshot.size()).isEqualTo(3); - - ExplainMetrics metrics = explainResults.getMetrics(); - assertThat(metrics).isNotNull(); - - PlanSummary planSummary = metrics.getPlanSummary(); - assertThat(planSummary).isNotNull(); - assertThat(planSummary.getIndexesUsed()).isNotEmpty(); - - ExecutionStats stats = metrics.getExecutionStats(); - assertThat(stats).isNotNull(); - assertThat(stats.getDebugStats()).isNotEmpty(); - assertThat(stats.getReadOperations()).isEqualTo(3); - assertThat(stats.getResultsReturned()).isEqualTo(3); - assertThat(stats.getExecutionDuration()).isGreaterThan(Duration.ZERO); - } - - @Test - public void testQueryProfileForQueryWithNoResultSet() throws Exception { - Map> testDocs = - map( - "doc1", map("a", 1, "b", asList(0)), - "doc2", map("b", asList(1)), - "doc3", map("a", 3, "b", asList(2, 7), "c", 10), - "doc4", map("a", 1, "b", asList(3, 7)), - "doc5", map("a", 1), - "doc6", map("a", 2, "c", 20)); - CollectionReference collection = testCollectionWithDocs(testDocs); - - Query query = collection.where(Filter.equalTo("a", 100)).orderBy("a"); - - // Regular query execution (get). - QuerySnapshot getSnapshot = query.get().get(); - assertThat(getSnapshot.size()).isEqualTo(0); - - // Explain. - ExplainResults explainResults = - query.explain(ExplainOptions.builder().setAnalyze(true).build()).get(); - - @Nullable QuerySnapshot snapshot = explainResults.getSnapshot(); - assertThat(snapshot).isNotNull(); - assertThat(snapshot.size()).isEqualTo(0); - - ExplainMetrics metrics = explainResults.getMetrics(); - assertThat(metrics).isNotNull(); - - PlanSummary planSummary = metrics.getPlanSummary(); - assertThat(planSummary).isNotNull(); - assertThat(planSummary.getIndexesUsed()).isNotEmpty(); - - ExecutionStats stats = metrics.getExecutionStats(); - assertThat(stats).isNotNull(); - assertThat(stats.getDebugStats()).isNotEmpty(); - assertThat(stats.getReadOperations()).isGreaterThan(0); - assertThat(stats.getResultsReturned()).isEqualTo(0); - assertThat(stats.getExecutionDuration()).isGreaterThan(Duration.ZERO); - } - - @Test - public void testExplainStreamWithoutAnalyze() throws Exception { - CollectionReference collection = testCollectionWithDocs(Collections.emptyMap()); - Query query = collection.where(Filter.equalTo("a", 1)).orderBy("a"); - - ApiFuture metricsFuture = - query.explainStream( - ExplainOptions.builder().setAnalyze(false).build(), - new ApiStreamObserver() { - @Override - public void onNext(DocumentSnapshot documentSnapshot) { - fail("No DocumentSnapshot should be received because analyze option was disabled."); - } - - @Override - public void onError(Throwable throwable) { - fail(throwable.getMessage()); - } - - @Override - public void onCompleted() {} - }); - - ExplainMetrics metrics = metricsFuture.get(); - assertThat(metrics.getPlanSummary().getIndexesUsed().size()).isGreaterThan(0); - assertThat(metrics.getExecutionStats()).isNull(); - } - - @Test - public void testExplainStreamWithAnalyze() throws Exception { - Map> testDocs = - map( - "doc1", map("a", 1, "b", asList(0)), - "doc2", map("b", asList(1)), - "doc3", map("a", 3, "b", asList(2, 7), "c", 10), - "doc4", map("a", 1, "b", asList(3, 7)), - "doc5", map("a", 1), - "doc6", map("a", 2, "c", 20)); - CollectionReference collection = testCollectionWithDocs(testDocs); - - Query query = collection.where(Filter.equalTo("a", 1)).orderBy("a"); - - final Iterator iterator = Arrays.asList("doc1", "doc4", "doc5").iterator(); - - ApiFuture metricsFuture = - query.explainStream( - ExplainOptions.builder().setAnalyze(true).build(), - new ApiStreamObserver() { - @Override - public void onNext(DocumentSnapshot documentSnapshot) { - assertEquals(iterator.next(), documentSnapshot.getId()); - } - - @Override - public void onError(Throwable throwable) { - fail(throwable.getMessage()); - } - - @Override - public void onCompleted() {} - }); - - ExplainMetrics metrics = metricsFuture.get(); - assertThat(metrics.getPlanSummary().getIndexesUsed().size()).isGreaterThan(0); - assertThat(metrics.getExecutionStats()).isNotNull(); - assertThat(metrics.getExecutionStats().getResultsReturned()).isEqualTo(3); - } - - @Test - public void testAggregateQueryPlan() throws Exception { - Map> testDocs = - map( - "doc1", map("a", 1, "b", asList(0)), - "doc2", map("b", asList(1)), - "doc3", map("a", 3, "b", asList(2, 7), "c", 10), - "doc4", map("a", 1, "b", asList(3, 7)), - "doc5", map("a", 1), - "doc6", map("a", 2, "c", 20)); - CollectionReference collection = testCollectionWithDocs(testDocs); - - AggregateQuery query = collection.where(Filter.equalTo("a", 1)).orderBy("a").count(); - - ExplainResults explainResults = - query.explain(ExplainOptions.builder().setAnalyze(false).build()).get(); - - @Nullable AggregateQuerySnapshot snapshot = explainResults.getSnapshot(); - assertThat(snapshot).isNull(); - - ExplainMetrics metrics = explainResults.getMetrics(); - assertThat(metrics).isNotNull(); - - PlanSummary planSummary = metrics.getPlanSummary(); - assertThat(planSummary).isNotNull(); - assertThat(planSummary.getIndexesUsed()).isNotEmpty(); - - ExecutionStats stats = metrics.getExecutionStats(); - assertThat(stats).isNull(); - } - - @Test - public void testAggregateQueryProfile() throws Exception { - Map> testDocs = - map( - "doc1", map("a", 1, "b", asList(0)), - "doc2", map("b", asList(1)), - "doc3", map("a", 3, "b", asList(2, 7), "c", 10), - "doc4", map("a", 1, "b", asList(3, 7)), - "doc5", map("a", 1), - "doc6", map("a", 2, "c", 20)); - CollectionReference collection = testCollectionWithDocs(testDocs); - - AggregateQuery query = collection.where(Filter.equalTo("a", 1)).orderBy("a").count(); - - ExplainResults explainResults = - query.explain(ExplainOptions.builder().setAnalyze(true).build()).get(); - - @Nullable AggregateQuerySnapshot snapshot = explainResults.getSnapshot(); - assertThat(snapshot).isNotNull(); - assertThat(snapshot.getCount()).isEqualTo(3); - - ExplainMetrics metrics = explainResults.getMetrics(); - assertThat(metrics).isNotNull(); - - PlanSummary planSummary = metrics.getPlanSummary(); - assertThat(planSummary).isNotNull(); - assertThat(planSummary.getIndexesUsed()).isNotEmpty(); - - ExecutionStats stats = metrics.getExecutionStats(); - assertThat(stats).isNotNull(); - assertThat(stats.getDebugStats()).isNotEmpty(); - assertThat(stats.getReadOperations()).isEqualTo(1); - assertThat(stats.getResultsReturned()).isEqualTo(1); - assertThat(stats.getExecutionDuration()).isGreaterThan(Duration.ZERO); + checkResultContainsDocumentsInOrder(query3, "doc2", "doc3", "doc4"); + checkResultContainsDocumentsInOrder(query3WithCursor, "doc2", "doc3", "doc4"); } @Test diff --git a/google-cloud-firestore/src/test/java/com/google/cloud/firestore/it/ITQueryToPipelineTest.java b/google-cloud-firestore/src/test/java/com/google/cloud/firestore/it/ITQueryToPipelineTest.java new file mode 100644 index 000000000..c9b5d450a --- /dev/null +++ b/google-cloud-firestore/src/test/java/com/google/cloud/firestore/it/ITQueryToPipelineTest.java @@ -0,0 +1,631 @@ +/* + * Copyright 2024 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.it; + +import static com.google.cloud.firestore.it.ITQueryTest.map; +import static org.junit.Assert.assertEquals; +import static org.junit.Assume.assumeTrue; + +import com.google.cloud.firestore.CollectionReference; +import com.google.cloud.firestore.DocumentSnapshot; +import com.google.cloud.firestore.FieldPath; +import com.google.cloud.firestore.Filter; +import com.google.cloud.firestore.LocalFirestoreHelper; +import com.google.cloud.firestore.PipelineResult; +import com.google.cloud.firestore.Query; +import com.google.common.collect.ImmutableMap; +import java.util.Arrays; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; +import java.util.stream.Collectors; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + +@RunWith(JUnit4.class) +public class ITQueryToPipelineTest extends ITBaseTest { + + public CollectionReference testCollectionWithDocs(Map> docs) + throws ExecutionException, InterruptedException, TimeoutException { + CollectionReference collection = firestore.collection(LocalFirestoreHelper.autoId()); + for (Map.Entry> doc : docs.entrySet()) { + collection.document(doc.getKey()).set(doc.getValue()).get(5, TimeUnit.SECONDS); + } + return collection; + } + + List> data(List results) { + return results.stream().map(PipelineResult::getData).collect(Collectors.toList()); + } + + @Before + public void setup() throws Exception { + assumeTrue( + "This test suite only runs against the Enterprise edition.", + getFirestoreEdition().equals(FirestoreEdition.ENTERPRISE)); + } + + private Object normalizeNumbers(Object value) { + if (value instanceof Number) { + if (value instanceof Double || value instanceof Float) { + return ((Number) value).doubleValue(); + } + return ((Number) value).longValue(); + } + if (value instanceof List) { + return ((List) value).stream().map(this::normalizeNumbers).collect(Collectors.toList()); + } + if (value instanceof Map) { + Map newMap = new HashMap<>(); + ((Map) value).forEach((k, v) -> newMap.put((String) k, normalizeNumbers(v))); + return newMap; + } + return value; + } + + private Map normalizeMap(Map map) { + Map newMap = new HashMap<>(); + map.forEach((key, value) -> newMap.put(key, normalizeNumbers(value))); + return newMap; + } + + private void verifyResults(List actual, Map... expected) { + List> actualData = data(actual); + assertEquals(expected.length, actualData.size()); + for (int i = 0; i < expected.length; ++i) { + Map expectedMap = normalizeMap(expected[i]); + Map actualMap = normalizeMap(actualData.get(i)); + assertEquals(expectedMap, actualMap); + } + } + + @Test + public void supportsDefaultQuery() throws Exception { + CollectionReference collRef = testCollectionWithDocs(ImmutableMap.of("1", map("foo", 1L))); + List snapshot = + firestore.pipeline().createFrom(collRef).execute().get().getResults(); + verifyResults(snapshot, map("foo", 1L)); + } + + @Test + public void supportsFilteredQuery() throws Exception { + CollectionReference collRef = + testCollectionWithDocs(ImmutableMap.of("1", map("foo", 1L), "2", map("foo", 2L))); + Query query1 = collRef.whereEqualTo("foo", 1L); + List snapshot = + firestore.pipeline().createFrom(query1).execute().get().getResults(); + verifyResults(snapshot, map("foo", 1L)); + } + + @Test + public void supportsFilteredQueryWithFieldPath() throws Exception { + CollectionReference collRef = + testCollectionWithDocs(ImmutableMap.of("1", map("foo", 1L), "2", map("foo", 2L))); + Query query1 = collRef.whereEqualTo(FieldPath.of("foo"), 1L); + List snapshot = + firestore.pipeline().createFrom(query1).execute().get().getResults(); + verifyResults(snapshot, map("foo", 1L)); + } + + @Test + public void supportsOrderedQueryWithDefaultOrder() throws Exception { + CollectionReference collRef = + testCollectionWithDocs(ImmutableMap.of("1", map("foo", 1L), "2", map("foo", 2L))); + Query query1 = collRef.orderBy("foo"); + List snapshot = + firestore.pipeline().createFrom(query1).execute().get().getResults(); + verifyResults(snapshot, map("foo", 1L), map("foo", 2L)); + } + + @Test + public void supportsOrderedQueryWithAsc() throws Exception { + CollectionReference collRef = + testCollectionWithDocs(ImmutableMap.of("1", map("foo", 1L), "2", map("foo", 2L))); + Query query1 = collRef.orderBy("foo", Query.Direction.ASCENDING); + List snapshot = + firestore.pipeline().createFrom(query1).execute().get().getResults(); + verifyResults(snapshot, map("foo", 1L), map("foo", 2L)); + } + + @Test + public void supportsOrderedQueryWithDesc() throws Exception { + CollectionReference collRef = + testCollectionWithDocs(ImmutableMap.of("1", map("foo", 1L), "2", map("foo", 2L))); + Query query1 = collRef.orderBy("foo", Query.Direction.DESCENDING); + List snapshot = + firestore.pipeline().createFrom(query1).execute().get().getResults(); + verifyResults(snapshot, map("foo", 2L), map("foo", 1L)); + } + + @Test + public void supportsLimitQuery() throws Exception { + CollectionReference collRef = + testCollectionWithDocs(ImmutableMap.of("1", map("foo", 1L), "2", map("foo", 2L))); + Query query1 = collRef.orderBy("foo").limit(1); + List snapshot = + firestore.pipeline().createFrom(query1).execute().get().getResults(); + verifyResults(snapshot, map("foo", 1L)); + } + + @Test + public void supportsLimitToLastQuery() throws Exception { + CollectionReference collRef = + testCollectionWithDocs( + ImmutableMap.of("1", map("foo", 1L), "2", map("foo", 2L), "3", map("foo", 3L))); + Query query1 = collRef.orderBy("foo").limitToLast(2); + List snapshot = + firestore.pipeline().createFrom(query1).execute().get().getResults(); + verifyResults(snapshot, map("foo", 2L), map("foo", 3L)); + } + + @Test + public void supportsStartAt() throws Exception { + CollectionReference collRef = + testCollectionWithDocs(ImmutableMap.of("1", map("foo", 1L), "2", map("foo", 2L))); + Query query1 = collRef.orderBy("foo").startAt(2L); + List snapshot = + firestore.pipeline().createFrom(query1).execute().get().getResults(); + verifyResults(snapshot, map("foo", 2L)); + } + + @Test + public void supportsStartAtWithLimitToLast() throws Exception { + CollectionReference collRef = + testCollectionWithDocs( + ImmutableMap.of( + "1", map("foo", 1L), + "2", map("foo", 2L), + "3", map("foo", 3L), + "4", map("foo", 4L), + "5", map("foo", 5L))); + Query query1 = collRef.orderBy("foo").startAt(3L).limitToLast(4); + List snapshot = + firestore.pipeline().createFrom(query1).execute().get().getResults(); + verifyResults(snapshot, map("foo", 3L), map("foo", 4L), map("foo", 5L)); + } + + @Test + public void supportsEndAtWithLimitToLast() throws Exception { + CollectionReference collRef = + testCollectionWithDocs( + ImmutableMap.of( + "1", map("foo", 1L), + "2", map("foo", 2L), + "3", map("foo", 3L), + "4", map("foo", 4L), + "5", map("foo", 5L))); + Query query1 = collRef.orderBy("foo").endAt(3L).limitToLast(2); + List snapshot = + firestore.pipeline().createFrom(query1).execute().get().getResults(); + verifyResults(snapshot, map("foo", 2L), map("foo", 3L)); + } + + @Test + public void supportsStartAfterWithDocumentSnapshot() throws Exception { + CollectionReference collRef = + testCollectionWithDocs( + ImmutableMap.>builder() + .put("1", map("id", 1L, "foo", 1L, "bar", 1L, "baz", 1L)) + .put("2", map("id", 2L, "foo", 1L, "bar", 1L, "baz", 2L)) + .put("3", map("id", 3L, "foo", 1L, "bar", 1L, "baz", 2L)) + .put("4", map("id", 4L, "foo", 1L, "bar", 2L, "baz", 1L)) + .put("5", map("id", 5L, "foo", 1L, "bar", 2L, "baz", 2L)) + .put("6", map("id", 6L, "foo", 1L, "bar", 2L, "baz", 2L)) + .put("7", map("id", 7L, "foo", 2L, "bar", 1L, "baz", 1L)) + .put("8", map("id", 8L, "foo", 2L, "bar", 1L, "baz", 2L)) + .put("9", map("id", 9L, "foo", 2L, "bar", 1L, "baz", 2L)) + .put("10", map("id", 10L, "foo", 2L, "bar", 2L, "baz", 1L)) + .put("11", map("id", 11L, "foo", 2L, "bar", 2L, "baz", 2L)) + .put("12", map("id", 12L, "foo", 2L, "bar", 2L, "baz", 2L)) + .build()); + DocumentSnapshot docRef = collRef.document("2").get().get(); + Query query1 = collRef.orderBy("foo").orderBy("bar").orderBy("baz").startAfter(docRef); + List snapshot = + firestore.pipeline().createFrom(query1).execute().get().getResults(); + verifyResults( + snapshot, + map("id", 3L, "foo", 1L, "bar", 1L, "baz", 2L), + map("id", 4L, "foo", 1L, "bar", 2L, "baz", 1L), + map("id", 5L, "foo", 1L, "bar", 2L, "baz", 2L), + map("id", 6L, "foo", 1L, "bar", 2L, "baz", 2L), + map("id", 7L, "foo", 2L, "bar", 1L, "baz", 1L), + map("id", 8L, "foo", 2L, "bar", 1L, "baz", 2L), + map("id", 9L, "foo", 2L, "bar", 1L, "baz", 2L), + map("id", 10L, "foo", 2L, "bar", 2L, "baz", 1L), + map("id", 11L, "foo", 2L, "bar", 2L, "baz", 2L), + map("id", 12L, "foo", 2L, "bar", 2L, "baz", 2L)); + } + + @Test + public void supportsStartAtWithDocumentSnapshot() throws Exception { + CollectionReference collRef = + testCollectionWithDocs( + ImmutableMap.>builder() + .put("1", map("id", 1L, "foo", 1L, "bar", 1L, "baz", 1L)) + .put("2", map("id", 2L, "foo", 1L, "bar", 1L, "baz", 2L)) + .put("3", map("id", 3L, "foo", 1L, "bar", 1L, "baz", 2L)) + .put("4", map("id", 4L, "foo", 1L, "bar", 2L, "baz", 1L)) + .put("5", map("id", 5L, "foo", 1L, "bar", 2L, "baz", 2L)) + .put("6", map("id", 6L, "foo", 1L, "bar", 2L, "baz", 2L)) + .put("7", map("id", 7L, "foo", 2L, "bar", 1L, "baz", 1L)) + .put("8", map("id", 8L, "foo", 2L, "bar", 1L, "baz", 2L)) + .put("9", map("id", 9L, "foo", 2L, "bar", 1L, "baz", 2L)) + .put("10", map("id", 10L, "foo", 2L, "bar", 2L, "baz", 1L)) + .put("11", map("id", 11L, "foo", 2L, "bar", 2L, "baz", 2L)) + .put("12", map("id", 12L, "foo", 2L, "bar", 2L, "baz", 2L)) + .build()); + DocumentSnapshot docRef = collRef.document("2").get().get(); + Query query1 = collRef.orderBy("foo").orderBy("bar").orderBy("baz").startAt(docRef); + List snapshot = + firestore.pipeline().createFrom(query1).execute().get().getResults(); + verifyResults( + snapshot, + map("id", 2L, "foo", 1L, "bar", 1L, "baz", 2L), + map("id", 3L, "foo", 1L, "bar", 1L, "baz", 2L), + map("id", 4L, "foo", 1L, "bar", 2L, "baz", 1L), + map("id", 5L, "foo", 1L, "bar", 2L, "baz", 2L), + map("id", 6L, "foo", 1L, "bar", 2L, "baz", 2L), + map("id", 7L, "foo", 2L, "bar", 1L, "baz", 1L), + map("id", 8L, "foo", 2L, "bar", 1L, "baz", 2L), + map("id", 9L, "foo", 2L, "bar", 1L, "baz", 2L), + map("id", 10L, "foo", 2L, "bar", 2L, "baz", 1L), + map("id", 11L, "foo", 2L, "bar", 2L, "baz", 2L), + map("id", 12L, "foo", 2L, "bar", 2L, "baz", 2L)); + } + + @Test + public void supportsStartAfter() throws Exception { + CollectionReference collRef = + testCollectionWithDocs(ImmutableMap.of("1", map("foo", 1L), "2", map("foo", 2L))); + Query query1 = collRef.orderBy("foo").startAfter(1L); + List snapshot = + firestore.pipeline().createFrom(query1).execute().get().getResults(); + verifyResults(snapshot, map("foo", 2L)); + } + + @Test + public void supportsEndAt() throws Exception { + CollectionReference collRef = + testCollectionWithDocs(ImmutableMap.of("1", map("foo", 1L), "2", map("foo", 2L))); + Query query1 = collRef.orderBy("foo").endAt(1L); + List snapshot = + firestore.pipeline().createFrom(query1).execute().get().getResults(); + verifyResults(snapshot, map("foo", 1L)); + } + + @Test + public void supportsEndBefore() throws Exception { + CollectionReference collRef = + testCollectionWithDocs(ImmutableMap.of("1", map("foo", 1L), "2", map("foo", 2L))); + Query query1 = collRef.orderBy("foo").endBefore(2L); + List snapshot = + firestore.pipeline().createFrom(query1).execute().get().getResults(); + verifyResults(snapshot, map("foo", 1L)); + } + + @Test + public void supportsPagination() throws Exception { + CollectionReference collRef = + testCollectionWithDocs(ImmutableMap.of("1", map("foo", 1L), "2", map("foo", 2L))); + Query query1 = collRef.orderBy("foo").limit(1); + List snapshot = + firestore.pipeline().createFrom(query1).execute().get().getResults(); + verifyResults(snapshot, map("foo", 1L)); + + Query query2 = query1.startAfter(snapshot.get(0).get("foo")); + snapshot = firestore.pipeline().createFrom(query2).execute().get().getResults(); + verifyResults(snapshot, map("foo", 2L)); + } + + @Test + public void supportsPaginationOnDocumentIds() throws Exception { + CollectionReference collRef = + testCollectionWithDocs(ImmutableMap.of("1", map("foo", 1L), "2", map("foo", 2L))); + Query query1 = collRef.orderBy("foo").orderBy(FieldPath.documentId()).limit(1); + List snapshot = + firestore.pipeline().createFrom(query1).execute().get().getResults(); + verifyResults(snapshot, map("foo", 1L)); + + Query query2 = + query1.startAfter(snapshot.get(0).get("foo"), snapshot.get(0).getReference().getId()); + snapshot = firestore.pipeline().createFrom(query2).execute().get().getResults(); + verifyResults(snapshot, map("foo", 2L)); + } + + @Test + public void supportsCollectionGroups() throws Exception { + CollectionReference collRef = testCollectionWithDocs(ImmutableMap.of()); + String collectionGroupId = collRef.getId() + "group"; + + firestore + .document(collRef.getId() + "/foo/" + collectionGroupId + "/doc1") + .set(map("foo", 1L)) + .get(); + firestore + .document(collRef.getId() + "/bar/baz/boo/" + collectionGroupId + "/doc2") + .set(map("bar", 1L)) + .get(); + + Query query1 = firestore.collectionGroup(collectionGroupId).orderBy(FieldPath.documentId()); + List snapshot = + firestore.pipeline().createFrom(query1).execute().get().getResults(); + + verifyResults(snapshot, map("bar", 1L), map("foo", 1L)); + } + + @Test + public void supportsQueryOverCollectionPathWithSpecialCharacters() throws Exception { + CollectionReference collRef = testCollectionWithDocs(ImmutableMap.of()); + CollectionReference collectionWithSpecials = + collRef.document("so!@#$%^&*()_+special").collection("so!@#$%^&*()_+special"); + collectionWithSpecials.add(map("foo", 1L)).get(); + collectionWithSpecials.add(map("foo", 2L)).get(); + + Query query = collectionWithSpecials.orderBy("foo", Query.Direction.ASCENDING); + List snapshot = + firestore.pipeline().createFrom(query).execute().get().getResults(); + + verifyResults(snapshot, map("foo", 1L), map("foo", 2L)); + } + + @Test + public void supportsMultipleInequalityOnSameField() throws Exception { + CollectionReference collRef = + testCollectionWithDocs( + ImmutableMap.>builder() + .put("01", map("id", 1L, "foo", 1L, "bar", 1L, "baz", 1L)) + .put("02", map("id", 2L, "foo", 1L, "bar", 1L, "baz", 2L)) + .put("03", map("id", 3L, "foo", 1L, "bar", 1L, "baz", 2L)) + .put("04", map("id", 4L, "foo", 1L, "bar", 2L, "baz", 1L)) + .put("05", map("id", 5L, "foo", 1L, "bar", 2L, "baz", 2L)) + .put("06", map("id", 6L, "foo", 1L, "bar", 2L, "baz", 2L)) + .put("07", map("id", 7L, "foo", 2L, "bar", 1L, "baz", 1L)) + .put("08", map("id", 8L, "foo", 2L, "bar", 1L, "baz", 2L)) + .put("09", map("id", 9L, "foo", 2L, "bar", 1L, "baz", 2L)) + .put("10", map("id", 10L, "foo", 2L, "bar", 2L, "baz", 1L)) + .put("11", map("id", 11L, "foo", 2L, "bar", 2L, "baz", 2L)) + .put("12", map("id", 12L, "foo", 2L, "bar", 2L, "baz", 2L)) + .build()); + Query query1 = + collRef.where( + Filter.and(Filter.greaterThan("id", 2L), Filter.lessThanOrEqualTo("id", 10L))); + List snapshot = + firestore.pipeline().createFrom(query1.orderBy("id")).execute().get().getResults(); + verifyResults( + snapshot, + map("id", 3L, "foo", 1L, "bar", 1L, "baz", 2L), + map("id", 4L, "foo", 1L, "bar", 2L, "baz", 1L), + map("id", 5L, "foo", 1L, "bar", 2L, "baz", 2L), + map("id", 6L, "foo", 1L, "bar", 2L, "baz", 2L), + map("id", 7L, "foo", 2L, "bar", 1L, "baz", 1L), + map("id", 8L, "foo", 2L, "bar", 1L, "baz", 2L), + map("id", 9L, "foo", 2L, "bar", 1L, "baz", 2L), + map("id", 10L, "foo", 2L, "bar", 2L, "baz", 1L)); + } + + @Test + public void supportsMultipleInequalityOnDifferentFields() throws Exception { + CollectionReference collRef = + testCollectionWithDocs( + ImmutableMap.>builder() + .put("01", map("id", 1L, "foo", 1L, "bar", 1L, "baz", 1L)) + .put("02", map("id", 2L, "foo", 1L, "bar", 1L, "baz", 2L)) + .put("03", map("id", 3L, "foo", 1L, "bar", 1L, "baz", 2L)) + .put("04", map("id", 4L, "foo", 1L, "bar", 2L, "baz", 1L)) + .put("05", map("id", 5L, "foo", 1L, "bar", 2L, "baz", 2L)) + .put("06", map("id", 6L, "foo", 1L, "bar", 2L, "baz", 2L)) + .put("07", map("id", 7L, "foo", 2L, "bar", 1L, "baz", 1L)) + .put("08", map("id", 8L, "foo", 2L, "bar", 1L, "baz", 2L)) + .put("09", map("id", 9L, "foo", 2L, "bar", 1L, "baz", 2L)) + .put("10", map("id", 10L, "foo", 2L, "bar", 2L, "baz", 1L)) + .put("11", map("id", 11L, "foo", 2L, "bar", 2L, "baz", 2L)) + .put("12", map("id", 12L, "foo", 2L, "bar", 2L, "baz", 2L)) + .build()); + Query query1 = + collRef.where( + Filter.and(Filter.greaterThanOrEqualTo("id", 2L), Filter.lessThan("baz", 2L))); + List snapshot = + firestore.pipeline().createFrom(query1.orderBy("id")).execute().get().getResults(); + verifyResults( + snapshot, + map("id", 4L, "foo", 1L, "bar", 2L, "baz", 1L), + map("id", 7L, "foo", 2L, "bar", 1L, "baz", 1L), + map("id", 10L, "foo", 2L, "bar", 2L, "baz", 1L)); + } + + @Test + public void supportsCollectionGroupQuery() throws Exception { + CollectionReference collRef = testCollectionWithDocs(ImmutableMap.of("1", map("foo", 1L))); + List snapshot = + firestore + .pipeline() + .createFrom(firestore.collectionGroup(collRef.getId())) + .execute() + .get() + .getResults(); + verifyResults(snapshot, map("foo", 1L)); + } + + @Test + public void supportsEqNan() throws Exception { + CollectionReference collRef = + testCollectionWithDocs( + ImmutableMap.of( + "1", map("foo", 1L, "bar", Double.NaN), + "2", map("foo", 2L, "bar", 1L), + "3", map("foo", 3L, "bar", "bar"))); + Query query1 = collRef.whereEqualTo("bar", Double.NaN); + List snapshot = + firestore.pipeline().createFrom(query1).execute().get().getResults(); + verifyResults(snapshot, map("foo", 1L, "bar", Double.NaN)); + } + + @Test + public void supportsNeqNan() throws Exception { + CollectionReference collRef = + testCollectionWithDocs( + ImmutableMap.of( + "1", map("foo", 1L, "bar", Double.NaN), + "2", map("foo", 2L, "bar", 1L), + "3", map("foo", 3L, "bar", "bar"))); + Query query1 = collRef.whereNotEqualTo("bar", Double.NaN).orderBy("foo"); + List snapshot = + firestore.pipeline().createFrom(query1).execute().get().getResults(); + verifyResults(snapshot, map("foo", 2L, "bar", 1L), map("foo", 3L, "bar", "bar")); + } + + @Test + public void supportsEqNull() throws Exception { + CollectionReference collRef = + testCollectionWithDocs( + ImmutableMap.of("1", map("foo", 1L, "bar", null), "2", map("foo", 2L, "bar", 1L))); + Query query1 = collRef.whereEqualTo("bar", null); + List snapshot = + firestore.pipeline().createFrom(query1).execute().get().getResults(); + verifyResults(snapshot, map("foo", 1L, "bar", null)); + } + + @Test + public void supportsNeqNull() throws Exception { + CollectionReference collRef = + testCollectionWithDocs( + ImmutableMap.of("1", map("foo", 1L, "bar", null), "2", map("foo", 2L, "bar", 1L))); + Query query1 = collRef.whereNotEqualTo("bar", null); + List snapshot = + firestore.pipeline().createFrom(query1).execute().get().getResults(); + verifyResults(snapshot, map("foo", 2L, "bar", 1L)); + } + + @Test + public void supportsNeq() throws Exception { + CollectionReference collRef = + testCollectionWithDocs( + ImmutableMap.of("1", map("foo", 1L, "bar", 0L), "2", map("foo", 2L, "bar", 1L))); + Query query1 = collRef.whereNotEqualTo("bar", 0L); + List snapshot = + firestore.pipeline().createFrom(query1).execute().get().getResults(); + verifyResults(snapshot, map("foo", 2L, "bar", 1L)); + } + + @Test + public void supportsArrayContains() throws Exception { + CollectionReference collRef = + testCollectionWithDocs( + ImmutableMap.of( + "1", map("foo", 1L, "bar", Arrays.asList(0L, 2L, 4L, 6L)), + "2", map("foo", 2L, "bar", Arrays.asList(1L, 3L, 5L, 7L)))); + Query query1 = collRef.whereArrayContains("bar", 4L); + List snapshot = + firestore.pipeline().createFrom(query1).execute().get().getResults(); + verifyResults(snapshot, map("foo", 1L, "bar", Arrays.asList(0L, 2L, 4L, 6L))); + } + + @Test + public void supportsArrayContainsAny() throws Exception { + CollectionReference collRef = + testCollectionWithDocs( + ImmutableMap.of( + "1", map("foo", 1L, "bar", Arrays.asList(0L, 2L, 4L, 6L)), + "2", map("foo", 2L, "bar", Arrays.asList(1L, 3L, 5L, 7L)), + "3", map("foo", 3L, "bar", Arrays.asList(10L, 20L, 30L, 40L)))); + Query query1 = collRef.whereArrayContainsAny("bar", Arrays.asList(4L, 5L)); + List snapshot = + firestore.pipeline().createFrom(query1.orderBy("foo")).execute().get().getResults(); + verifyResults( + snapshot, + map("foo", 1L, "bar", Arrays.asList(0L, 2L, 4L, 6L)), + map("foo", 2L, "bar", Arrays.asList(1L, 3L, 5L, 7L))); + } + + @Test + public void supportsIn() throws Exception { + CollectionReference collRef = + testCollectionWithDocs( + ImmutableMap.of( + "1", map("foo", 1L, "bar", 2L), + "2", map("foo", 2L), + "3", map("foo", 3L, "bar", 10L))); + Query query1 = collRef.whereIn("bar", Arrays.asList(0L, 10L, 20L)); + List snapshot = + firestore.pipeline().createFrom(query1).execute().get().getResults(); + verifyResults(snapshot, map("foo", 3L, "bar", 10L)); + } + + @Test + public void supportsInWith1() throws Exception { + CollectionReference collRef = + testCollectionWithDocs( + ImmutableMap.of( + "1", map("foo", 1L, "bar", 2L), + "2", map("foo", 2L), + "3", map("foo", 3L, "bar", 10L))); + Query query1 = collRef.whereIn("bar", Arrays.asList(2L)); + List snapshot = + firestore.pipeline().createFrom(query1).execute().get().getResults(); + verifyResults(snapshot, map("foo", 1L, "bar", 2L)); + } + + @Test + public void supportsNotIn() throws Exception { + CollectionReference collRef = + testCollectionWithDocs( + ImmutableMap.of( + "1", map("foo", 1L, "bar", 2L), + "2", map("foo", 2L, "bar", 1L), + "3", map("foo", 3L, "bar", 10L))); + Query query1 = collRef.whereNotIn("bar", Arrays.asList(0L, 10L, 20L)).orderBy("foo"); + List snapshot = + firestore.pipeline().createFrom(query1).execute().get().getResults(); + verifyResults(snapshot, map("foo", 1L, "bar", 2L), map("foo", 2L, "bar", 1L)); + } + + @Test + public void supportsNotInWith1() throws Exception { + CollectionReference collRef = + testCollectionWithDocs( + ImmutableMap.of( + "1", map("foo", 1L, "bar", 2L), + "2", map("foo", 2L), + "3", map("foo", 3L, "bar", 10L))); + Query query1 = collRef.whereNotIn("bar", Arrays.asList(2L)).orderBy("foo"); + List snapshot = + firestore.pipeline().createFrom(query1).execute().get().getResults(); + verifyResults(snapshot, map("foo", 3L, "bar", 10L)); + } + + @Test + public void supportsOrOperator() throws Exception { + CollectionReference collRef = + testCollectionWithDocs( + ImmutableMap.of( + "1", map("foo", 1L, "bar", 2L), + "2", map("foo", 2L, "bar", 0L), + "3", map("foo", 3L, "bar", 10L))); + Query query1 = + collRef + .where(Filter.or(Filter.equalTo("bar", 2L), Filter.equalTo("foo", 3L))) + .orderBy("foo"); + List snapshot = + firestore.pipeline().createFrom(query1).execute().get().getResults(); + verifyResults(snapshot, map("foo", 1L, "bar", 2L), map("foo", 3L, "bar", 10L)); + } +} diff --git a/google-cloud-firestore/src/test/java/com/google/cloud/firestore/it/ITShutdownTest.java b/google-cloud-firestore/src/test/java/com/google/cloud/firestore/it/ITShutdownTest.java index 6683c3350..f6ca62e9d 100644 --- a/google-cloud-firestore/src/test/java/com/google/cloud/firestore/it/ITShutdownTest.java +++ b/google-cloud-firestore/src/test/java/com/google/cloud/firestore/it/ITShutdownTest.java @@ -17,6 +17,7 @@ package com.google.cloud.firestore.it; import static org.junit.Assert.fail; +import static org.junit.Assume.assumeFalse; import com.google.api.core.SettableApiFuture; import com.google.cloud.firestore.Firestore; @@ -50,6 +51,13 @@ public void closeSuccess_withListenerRemove() throws Exception { @Test public void closeFailure_withoutListenerRemove() throws Exception { + // TODO(pipeline): This test fails against emulator, suggesting the test setup probably depends + // on timing. + // We should fix it. + assumeFalse( + "Skip this test when running against the emulator because it depends on timing.", + TestHelper.isRunningAgainstFirestoreEmulator(firestore)); + final Firestore fs = FirestoreOptions.getDefaultInstance().getService(); attachListener(fs); diff --git a/google-cloud-firestore/src/test/java/com/google/cloud/firestore/it/ITSystemTest.java b/google-cloud-firestore/src/test/java/com/google/cloud/firestore/it/ITSystemTest.java index 0281f58a9..8509d6045 100644 --- a/google-cloud-firestore/src/test/java/com/google/cloud/firestore/it/ITSystemTest.java +++ b/google-cloud-firestore/src/test/java/com/google/cloud/firestore/it/ITSystemTest.java @@ -37,6 +37,7 @@ import static org.junit.Assert.assertThrows; import static org.junit.Assert.assertTrue; import static org.junit.Assert.fail; +import static org.junit.Assume.assumeFalse; import static org.junit.Assume.assumeTrue; import com.google.api.core.ApiFuture; @@ -905,6 +906,12 @@ public void endBefore() throws Exception { @Test public void partitionedQuery() throws Exception { + // Partitioned queries are not supported in the emulator. + assumeFalse( + "Skip this test when running against the Firestore emulator because it does not support" + + " partitioned queries.", + isRunningAgainstFirestoreEmulator(firestore)); + int documentCount = 2 * 128 + 127; // Minimum partition size is 128. WriteBatch batch = firestore.batch(); @@ -933,6 +940,12 @@ public void partitionedQuery() throws Exception { @Test public void partitionedQuery_future() throws Exception { + // Partitioned queries are not supported in the emulator. + assumeFalse( + "Skip this test when running against the Firestore emulator because it does not support" + + " partitioned queries.", + isRunningAgainstFirestoreEmulator(firestore)); + int documentCount = 2 * 128 + 127; // Minimum partition size is 128. WriteBatch batch = firestore.batch(); @@ -961,6 +974,12 @@ public void partitionedQuery_future() throws Exception { @Test public void emptyPartitionedQuery() throws Exception { + // Partitioned queries are not supported in the emulator. + assumeFalse( + "Skip this test when running against the Firestore emulator because it does not support" + + " partitioned queries.", + isRunningAgainstFirestoreEmulator(firestore)); + StreamConsumer consumer = new StreamConsumer<>(); firestore.collectionGroup(randomColl.getId()).getPartitions(3, consumer); final List partitions = consumer.consume().get(); @@ -1074,7 +1093,7 @@ public void successfulTransactionWithContention() throws Exception { assertEquals("foo", firstTransaction.get()); assertEquals("bar", secondTransaction.get()); - assertEquals(3, attempts.intValue()); + assertThat(attempts.intValue()).isAtLeast(3); assertEquals(3, (long) documentReference.get().get().getLong("counter")); } @@ -1992,6 +2011,11 @@ public void readOnlyTransaction_successfulRead() throws Exception { @Test public void readOnlyTransaction_failureWhenAttemptReadOlderThan60Seconds() throws ExecutionException, InterruptedException, TimeoutException { + // Skip this test because emulator does not have this behavior. + assumeFalse( + "Skip this test when running against the emulator because it does not have this behavior.", + TestHelper.isRunningAgainstFirestoreEmulator(firestore)); + final DocumentReference documentReference = randomColl.add(SINGLE_FIELD_MAP).get(); // Exception isn't thrown until 60 minutes. diff --git a/google-cloud-firestore/src/test/java/com/google/cloud/firestore/it/ITTracingTest.java b/google-cloud-firestore/src/test/java/com/google/cloud/firestore/it/ITTracingTest.java index 7efdd0704..2d4df9a35 100644 --- a/google-cloud-firestore/src/test/java/com/google/cloud/firestore/it/ITTracingTest.java +++ b/google-cloud-firestore/src/test/java/com/google/cloud/firestore/it/ITTracingTest.java @@ -21,6 +21,7 @@ import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertTrue; +import static org.junit.Assume.assumeFalse; import com.google.cloud.firestore.BulkWriter; import com.google.cloud.firestore.BulkWriterOptions; @@ -147,6 +148,10 @@ public void before() { } firestore = optionsBuilder.build().getService(); + assumeFalse( + "ITTracingTest is not supported against the emulator.", + "EMULATOR".equals(ITBaseTest.getTargetBackend())); + // Clean up existing maps. spanNameToSpanId.clear(); spanIdToParentSpanId.clear(); diff --git a/google-cloud-firestore/src/test/java/com/google/cloud/firestore/it/TestHelper.java b/google-cloud-firestore/src/test/java/com/google/cloud/firestore/it/TestHelper.java index 54cfd86f0..ad851e4be 100644 --- a/google-cloud-firestore/src/test/java/com/google/cloud/firestore/it/TestHelper.java +++ b/google-cloud-firestore/src/test/java/com/google/cloud/firestore/it/TestHelper.java @@ -28,7 +28,7 @@ private TestHelper() {} /** Returns whether the tests are running against the Firestore emulator. */ static boolean isRunningAgainstFirestoreEmulator(Firestore firestore) { - return firestore.getOptions().getHost().startsWith("localhost:"); + return firestore.getOptions().getEmulatorHost() != null; } /** diff --git a/samples/preview-snippets/pom.xml b/samples/preview-snippets/pom.xml new file mode 100644 index 000000000..82132b2fb --- /dev/null +++ b/samples/preview-snippets/pom.xml @@ -0,0 +1,87 @@ + + + 4.0.0 + com.google.cloud + firestore-snippets + jar + Google Cloud Firestore Snippets + https://github.com/googleapis/java-firestore + + + + com.google.cloud.samples + shared-configuration + 1.2.0 + + + + 1.8 + 1.8 + UTF-8 + 2.57.0 + + + + + + + + + com.google.cloud + google-cloud-firestore-bom + + 99.99.0-PRIVATEPREVIEW + pom + import + + + + + + + + com.google.firebase + firebase-admin + 9.7.0 + + + + + + + org.codehaus.mojo + exec-maven-plugin + 3.3.0 + + + + quickstart + + exec + + + java + + -classpath + + com.example.firestore.Quickstart + ${firestore.project.id} + + + + + beam-sample + + exec + + + + + + + diff --git a/samples/preview-snippets/src/main/java/com/example/firestore/PipelineSnippets.java b/samples/preview-snippets/src/main/java/com/example/firestore/PipelineSnippets.java new file mode 100644 index 000000000..aeb373747 --- /dev/null +++ b/samples/preview-snippets/src/main/java/com/example/firestore/PipelineSnippets.java @@ -0,0 +1,2962 @@ +/* + * Copyright 2025 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.example.firestore; + +import static com.google.cloud.firestore.pipeline.expressions.AggregateFunction.*; +import static com.google.cloud.firestore.pipeline.expressions.Expression.*; +import static com.google.cloud.firestore.pipeline.expressions.Ordering.*; + +import com.google.api.core.ApiFuture; +import com.google.api.gax.rpc.ApiStreamObserver; +import com.google.cloud.firestore.ExplainMetrics; +import com.google.cloud.firestore.ExplainOptions; +import com.google.cloud.firestore.ExplainResults; +import com.google.cloud.firestore.Firestore; +import com.google.cloud.firestore.FirestoreOptions; +import com.google.cloud.firestore.Pipeline; +import com.google.cloud.firestore.PlanSummary; +import com.google.cloud.firestore.Query; +import com.google.cloud.firestore.QuerySnapshot; +import com.google.cloud.firestore.pipeline.stages.Aggregate; +import com.google.cloud.firestore.pipeline.stages.FindNearest; +import com.google.cloud.firestore.pipeline.stages.FindNearestOptions; +import com.google.cloud.firestore.pipeline.stages.Sample; +import com.google.cloud.firestore.pipeline.stages.UnnestOptions; +import java.util.Arrays; +import java.util.HashMap; +import java.util.concurrent.ExecutionException; + +class PipelineSnippets { + private final Firestore firestore; + + PipelineSnippets(Firestore firestore) { + this.firestore = firestore; + } + + void queryExplain() throws ExecutionException, InterruptedException { + // [START query_explain] + Query q = firestore.collection("cities").whereGreaterThan("population", 1); + ExplainOptions options = ExplainOptions.builder().build(); + + ExplainResults explainResults = q.explain(options).get(); + ExplainMetrics metrics = explainResults.getMetrics(); + PlanSummary planSummary = metrics.getPlanSummary(); + // [END query_explain] + } + + void pipelineConcepts() throws ExecutionException, InterruptedException { + // [START pipeline_concepts] + Pipeline pipeline = + firestore + .pipeline() + .collection("cities") + .where(field("population").greaterThan(100_000)) + .sort(ascending(field("name"))) + .limit(10); + // [END pipeline_concepts] + System.out.println(pipeline); + } + + void basicRead() throws ExecutionException, InterruptedException { + // [START basic_read] + Pipeline pipeline = firestore.pipeline().collection("users"); + ApiFuture future = pipeline.execute(); + for (com.google.cloud.firestore.PipelineResult result : future.get().getResults()) { + System.out.println(result.getId() + " => " + result.getData()); + } + // or, asynchronously + pipeline.execute( + new ApiStreamObserver() { + @Override + public void onNext(com.google.cloud.firestore.PipelineResult result) { + System.out.println(result.getId() + " => " + result.getData()); + } + + @Override + public void onError(Throwable t) { + System.err.println(t); + } + + @Override + public void onCompleted() { + System.out.println("done"); + } + }); + // [END basic_read] + } + + void pipelineInitialization() { + // [START pipeline_initialization] + FirestoreOptions firestoreOptions = firestore.getOptions(); + Firestore firestoreClient = firestoreOptions.getService(); + Pipeline pipeline = firestoreClient.pipeline().collection("books"); + // [END pipeline_initialization] + System.out.println(pipeline); + } + + void fieldVsConstants() { + // [START field_or_constant] + Pipeline pipeline = + firestore.pipeline().collection("cities").where(field("name").equal(constant("Toronto"))); + // [END field_or_constant] + System.out.println(pipeline); + } + + void inputStages() throws ExecutionException, InterruptedException { + // [START input_stages] + // Return all restaurants in San Francisco + Pipeline.Snapshot results1 = + firestore.pipeline().collection("cities/sf/restaurants").execute().get(); + + // Return all restaurants + Pipeline.Snapshot results2 = + firestore.pipeline().collectionGroup("restaurants").execute().get(); + + // Return all documents across all collections in the database (the entire database) + Pipeline.Snapshot results3 = firestore.pipeline().database().execute().get(); + + // Batch read of 3 documents + Pipeline.Snapshot results4 = + firestore + .pipeline() + .documents( + firestore.collection("cities").document("SF"), + firestore.collection("cities").document("DC"), + firestore.collection("cities").document("NY")) + .execute() + .get(); + // [END input_stages] + System.out.println(results1.getResults()); + System.out.println(results2.getResults()); + System.out.println(results3.getResults()); + System.out.println(results4.getResults()); + } + + void wherePipeline() throws ExecutionException, InterruptedException { + // [START pipeline_where] + Pipeline.Snapshot results1 = + firestore + .pipeline() + .collection("books") + .where(field("rating").equal(5)) + .where(field("published").lessThan(1900)) + .execute() + .get(); + + Pipeline.Snapshot results2 = + firestore + .pipeline() + .collection("books") + .where(and(field("rating").equal(5), field("published").lessThan(1900))) + .execute() + .get(); + // [END pipeline_where] + System.out.println(results1.getResults()); + System.out.println(results2.getResults()); + } + + void aggregateGroups() throws ExecutionException, InterruptedException { + // [START aggregate_groups] + Pipeline.Snapshot results = + firestore + .pipeline() + .collection("books") + .aggregate( + Aggregate.withAccumulators(average("rating").as("avg_rating")).withGroups("genre")) + .execute() + .get(); + // [END aggregate_groups] + System.out.println(results.getResults()); + } + + void aggregateDistinct() throws ExecutionException, InterruptedException { + // [START aggregate_distinct] + Pipeline.Snapshot results = + firestore + .pipeline() + .collection("books") + .distinct(toUpper(field("author")).as("author"), field("genre")) + .execute() + .get(); + // [END aggregate_distinct] + System.out.println(results.getResults()); + } + + void sort() throws ExecutionException, InterruptedException { + // [START sort] + Pipeline.Snapshot results = + firestore + .pipeline() + .collection("books") + .sort(descending(field("release_date")), ascending(field("author"))) + .execute() + .get(); + // [END sort] + System.out.println(results.getResults()); + } + + void sortComparison() { + // [START sort_comparison] + Query query = + firestore + .collection("cities") + .orderBy("state") + .orderBy("population", Query.Direction.DESCENDING); + + Pipeline pipeline = + firestore + .pipeline() + .collection("books") + .sort(descending(field("release_date")), ascending(field("author"))); + // [END sort_comparison] + System.out.println(query); + System.out.println(pipeline); + } + + void functionsExample() throws ExecutionException, InterruptedException { + // [START functions_example] + // Type 1: Scalar (for use in non-aggregation stages) + // Example: Return the min store price for each book. + Pipeline.Snapshot results1 = + firestore + .pipeline() + .collection("books") + .select(logicalMinimum(field("current"), field("updated")).as("price_min")) + .execute() + .get(); + + // Type 2: Aggregation (for use in aggregate stages) + // Example: Return the min price of all books. + Pipeline.Snapshot results2 = + firestore + .pipeline() + .collection("books") + .aggregate(minimum("price").as("min_price")) + .execute() + .get(); + // [END functions_example] + System.out.println(results1.getResults()); + System.out.println(results2.getResults()); + } + + void creatingIndexes() throws ExecutionException, InterruptedException { + // [START query_example] + Pipeline.Snapshot results = + firestore + .pipeline() + .collection("books") + .where(field("published").lessThan(1900)) + .where(field("genre").equal("Science Fiction")) + .where(field("rating").greaterThan(4.3)) + .sort(descending(field("published"))) + .execute() + .get(); + // [END query_example] + System.out.println(results.getResults()); + } + + void sparseIndexes() throws ExecutionException, InterruptedException { + // [START sparse_index_example] + Pipeline.Snapshot results = + firestore + .pipeline() + .collection("books") + .where(field("category").like("%fantasy%")) + .execute() + .get(); + // [END sparse_index_example] + System.out.println(results.getResults()); + } + + void sparseIndexes2() throws ExecutionException, InterruptedException { + // [START sparse_index_example_2] + Pipeline.Snapshot results = + firestore + .pipeline() + .collection("books") + .sort(ascending(field("release_date"))) + .execute() + .get(); + // [END sparse_index_example_2] + System.out.println(results.getResults()); + } + + void coveredQuery() throws ExecutionException, InterruptedException { + // [START covered_query] + Pipeline.Snapshot results = + firestore + .pipeline() + .collection("books") + .where(field("category").like("%fantasy%")) + .where(field("title").exists()) + .where(field("author").exists()) + .select("title", "author") + .execute() + .get(); + // [END covered_query] + System.out.println(results.getResults()); + } + + void pagination() { + // [START pagination_not_supported_preview] + // Existing pagination via `start_at()` + Query query = firestore.collection("cities").orderBy("population").startAt(1_000_000); + + // Private preview workaround using pipelines + Pipeline pipeline = + firestore + .pipeline() + .collection("cities") + .where(field("population").greaterThanOrEqual(1_000_000)) + .sort(descending(field("population"))); + // [END pagination_not_supported_preview] + System.out.println(query); + System.out.println(pipeline); + } + + void collectionStage() throws ExecutionException, InterruptedException { + // [START collection_example] + Pipeline.Snapshot results = + firestore + .pipeline() + .collection("users/bob/games") + .sort(ascending(field("name"))) + .execute() + .get(); + // [END collection_example] + System.out.println(results.getResults()); + } + + void collectionGroupStage() throws ExecutionException, InterruptedException { + // [START collection_group_example] + Pipeline.Snapshot results = + firestore + .pipeline() + .collectionGroup("games") + .sort(ascending(field("name"))) + .execute() + .get(); + // [END collection_group_example] + System.out.println(results.getResults()); + } + + void databaseStage() throws ExecutionException, InterruptedException { + // [START database_example] + // Count all documents in the database + Pipeline.Snapshot results = + firestore.pipeline().database().aggregate(countAll().as("total")).execute().get(); + // [END database_example] + System.out.println(results.getResults()); + } + + void documentsStage() throws ExecutionException, InterruptedException { + // [START documents_example] + Pipeline.Snapshot results = + firestore + .pipeline() + .documents( + firestore.collection("cities").document("SF"), + firestore.collection("cities").document("DC"), + firestore.collection("cities").document("NY")) + .execute() + .get(); + // [END documents_example] + System.out.println(results.getResults()); + } + + void replaceWithStage() throws ExecutionException, InterruptedException { + // [START initial_data] + firestore + .collection("cities") + .document("SF") + .set( + new HashMap() { + { + put("name", "San Francisco"); + put("population", 800_000); + put( + "location", + new HashMap() { + { + put("country", "USA"); + put("state", "California"); + } + }); + } + }); + firestore + .collection("cities") + .document("TO") + .set( + new HashMap() { + { + put("name", "Toronto"); + put("population", 3_000_000); + put("province", "ON"); + put( + "location", + new HashMap() { + { + put("country", "Canada"); + put("province", "Ontario"); + } + }); + } + }); + firestore + .collection("cities") + .document("NY") + .set( + new HashMap() { + { + put("name", "New York"); + put("population", 8_500_000); + put( + "location", + new HashMap() { + { + put("country", "USA"); + put("state", "New York"); + } + }); + } + }); + firestore + .collection("cities") + .document("AT") + .set( + new HashMap() { + { + put("name", "Atlantis"); + } + }); + // [END initial_data] + + // [START full_replace] + Pipeline.Snapshot names = + firestore.pipeline().collection("cities").replaceWith(field("location")).execute().get(); + // [END full_replace] + + // [START map_merge_overwrite] + // unsupported in client SDKs for now + // [END map_merge_overwrite] + System.out.println(names.getResults()); + } + + void sampleStage() throws ExecutionException, InterruptedException { + // [START sample_example] + // Get a sample of 100 documents in a database + Pipeline.Snapshot results1 = firestore.pipeline().database().sample(100).execute().get(); + + // Randomly shuffle a list of 3 documents + Pipeline.Snapshot results2 = + firestore + .pipeline() + .documents( + firestore.collection("cities").document("SF"), + firestore.collection("cities").document("NY"), + firestore.collection("cities").document("DC")) + .sample(3) + .execute() + .get(); + // [END sample_example] + System.out.println(results1.getResults()); + System.out.println(results2.getResults()); + } + + void samplePercent() throws ExecutionException, InterruptedException { + // [START sample_percent] + // Get a sample of on average 50% of the documents in the database + Pipeline.Snapshot results = + firestore.pipeline().database().sample(Sample.withPercentage(0.5)).execute().get(); + // [END sample_percent] + System.out.println(results.getResults()); + } + + void unionStage() throws ExecutionException, InterruptedException { + // [START union_stage] + Pipeline.Snapshot results = + firestore + .pipeline() + .collection("cities/SF/restaurants") + .where(field("type").equal("Chinese")) + .union( + firestore + .pipeline() + .collection("cities/NY/restaurants") + .where(field("type").equal("Italian"))) + .where(field("rating").greaterThanOrEqual(4.5)) + .sort(descending(field("__name__"))) + .execute() + .get(); + // [END union_stage] + System.out.println(results.getResults()); + } + + void unionStageStable() throws ExecutionException, InterruptedException { + // [START union_stage_stable] + Pipeline.Snapshot results = + firestore + .pipeline() + .collection("cities/SF/restaurants") + .where(field("type").equal("Chinese")) + .union( + firestore + .pipeline() + .collection("cities/NY/restaurants") + .where(field("type").equal("Italian"))) + .where(field("rating").greaterThanOrEqual(4.5)) + .sort(descending(field("__name__"))) + .execute() + .get(); + // [END union_stage_stable] + System.out.println(results.getResults()); + } + + void unnestStage() throws ExecutionException, InterruptedException { + // [START unnest_stage] + Pipeline.Snapshot results = + firestore + .pipeline() + .database() + .unnest("arrayField", "unnestedArrayField", new UnnestOptions().withIndexField("index")) + .execute() + .get(); + // [END unnest_stage] + System.out.println(results.getResults()); + } + + void unnestStageEmptyOrNonArray() throws ExecutionException, InterruptedException { + // [START unnest_edge_cases] + // Input + // { "identifier" : 1, "neighbors": [ "Alice", "Cathy" ] } + // { "identifier" : 2, "neighbors": [] } + // { "identifier" : 3, "neighbors": "Bob" } + + Pipeline.Snapshot results = + firestore + .pipeline() + .database() + .unnest("neighbors", "unnestedNeighbors", new UnnestOptions().withIndexField("index")) + .execute() + .get(); + + // Output + // { "identifier": 1, "neighbors": [ "Alice", "Cathy" ], + // "unnestedNeighbors": "Alice", "index": 0 } + // { "identifier": 1, "neighbors": [ "Alice", "Cathy" ], + // "unnestedNeighbors": "Cathy", "index": 1 } + // { "identifier": 3, "neighbors": "Bob", "index": null} + // [END unnest_edge_cases] + System.out.println(results.getResults()); + } + + void countFunction() throws ExecutionException, InterruptedException { + // [START count_function] + // Total number of books in the collection + Pipeline.Snapshot countAll = + firestore.pipeline().collection("books").aggregate(countAll().as("count")).execute().get(); + + // Number of books with nonnull `ratings` field + Pipeline.Snapshot countField = + firestore + .pipeline() + .collection("books") + .aggregate(count("ratings").as("count")) + .execute() + .get(); + // [END count_function] + System.out.println(countAll.getResults()); + System.out.println(countField.getResults()); + } + + void countIfFunction() throws ExecutionException, InterruptedException { + // [START count_if] + Pipeline.Snapshot result = + firestore + .pipeline() + .collection("books") + .aggregate(countIf(field("rating").greaterThan(4)).as("filteredCount")) + .execute() + .get(); + // [END count_if] + System.out.println(result.getResults()); + } + + void countDistinctFunction() throws ExecutionException, InterruptedException { + // [START count_distinct] + Pipeline.Snapshot result = + firestore + .pipeline() + .collection("books") + .aggregate(countDistinct("author").as("unique_authors")) + .execute() + .get(); + // [END count_distinct] + System.out.println(result.getResults()); + } + + void sumFunction() throws ExecutionException, InterruptedException { + // [START sum_function] + Pipeline.Snapshot result = + firestore + .pipeline() + .collection("cities") + .aggregate(sum("population").as("totalPopulation")) + .execute() + .get(); + // [END sum_function] + System.out.println(result.getResults()); + } + + void avgFunction() throws ExecutionException, InterruptedException { + // [START avg_function] + Pipeline.Snapshot result = + firestore + .pipeline() + .collection("cities") + .aggregate(average("population").as("averagePopulation")) + .execute() + .get(); + // [END avg_function] + System.out.println(result.getResults()); + } + + void minFunction() throws ExecutionException, InterruptedException { + // [START min_function] + Pipeline.Snapshot result = + firestore + .pipeline() + .collection("books") + .aggregate(minimum("price").as("minimumPrice")) + .execute() + .get(); + // [END min_function] + System.out.println(result.getResults()); + } + + void maxFunction() throws ExecutionException, InterruptedException { + // [START max_function] + Pipeline.Snapshot result = + firestore + .pipeline() + .collection("books") + .aggregate(maximum("price").as("maximumPrice")) + .execute() + .get(); + // [END max_function] + System.out.println(result.getResults()); + } + + void addFunction() throws ExecutionException, InterruptedException { + // [START add_function] + Pipeline.Snapshot result = + firestore + .pipeline() + .collection("books") + .select(add(field("soldBooks"), field("unsoldBooks")).as("totalBooks")) + .execute() + .get(); + // [END add_function] + System.out.println(result.getResults()); + } + + void subtractFunction() throws ExecutionException, InterruptedException { + // [START subtract_function] + int storeCredit = 7; + Pipeline.Snapshot result = + firestore + .pipeline() + .collection("books") + .select(subtract(field("price"), storeCredit).as("totalCost")) + .execute() + .get(); + // [END subtract_function] + System.out.println(result.getResults()); + } + + void multiplyFunction() throws ExecutionException, InterruptedException { + // [START multiply_function] + Pipeline.Snapshot result = + firestore + .pipeline() + .collection("books") + .select(multiply(field("price"), field("soldBooks")).as("revenue")) + .execute() + .get(); + // [END multiply_function] + System.out.println(result.getResults()); + } + + void divideFunction() throws ExecutionException, InterruptedException { + // [START divide_function] + Pipeline.Snapshot result = + firestore + .pipeline() + .collection("books") + .select(divide(field("ratings"), field("soldBooks")).as("reviewRate")) + .execute() + .get(); + // [END divide_function] + System.out.println(result.getResults()); + } + + void modFunction() throws ExecutionException, InterruptedException { + // [START mod_function] + int displayCapacity = 1000; + Pipeline.Snapshot result = + firestore + .pipeline() + .collection("books") + .select(mod(field("unsoldBooks"), displayCapacity).as("warehousedBooks")) + .execute() + .get(); + // [END mod_function] + System.out.println(result.getResults()); + } + + void ceilFunction() throws ExecutionException, InterruptedException { + // [START ceil_function] + int booksPerShelf = 100; + Pipeline.Snapshot result = + firestore + .pipeline() + .collection("books") + .select(ceil(divide(field("unsoldBooks"), booksPerShelf)).as("requiredShelves")) + .execute() + .get(); + // [END ceil_function] + System.out.println(result.getResults()); + } + + void floorFunction() throws ExecutionException, InterruptedException { + // [START floor_function] + Pipeline.Snapshot result = + firestore + .pipeline() + .collection("books") + .addFields(floor(divide(field("wordCount"), field("pages"))).as("wordsPerPage")) + .execute() + .get(); + // [END floor_function] + System.out.println(result.getResults()); + } + + void roundFunction() throws ExecutionException, InterruptedException { + // [START round_function] + Pipeline.Snapshot result = + firestore + .pipeline() + .collection("books") + .select(round(multiply(field("soldBooks"), field("price"))).as("partialRevenue")) + .aggregate(sum("partialRevenue").as("totalRevenue")) + .execute() + .get(); + // [END round_function] + System.out.println(result.getResults()); + } + + void powFunction() throws ExecutionException, InterruptedException { + // [START pow_function] + double googleplexLat = 37.4221; + double googleplexLng = -122.0853; + Pipeline.Snapshot result = + firestore + .pipeline() + .collection("cities") + .addFields( + pow(multiply(subtract(field("lat"), googleplexLat), 111), 2) + .as("latitudeDifference"), + pow(multiply(subtract(field("lng"), googleplexLng), 111), 2) + .as("longitudeDifference")) + .select( + sqrt(add(field("latitudeDifference"), field("longitudeDifference"))) + // Inaccurate for large distances or close to poles + .as("approximateDistanceToGoogle")) + .execute() + .get(); + // [END pow_function] + System.out.println(result.getResults()); + } + + void sqrtFunction() throws ExecutionException, InterruptedException { + // [START sqrt_function] + double googleplexLat = 37.4221; + double googleplexLng = -122.0853; + Pipeline.Snapshot result = + firestore + .pipeline() + .collection("cities") + .addFields( + pow(multiply(subtract(field("lat"), googleplexLat), 111), 2) + .as("latitudeDifference"), + pow(multiply(subtract(field("lng"), googleplexLng), 111), 2) + .as("longitudeDifference")) + .select( + sqrt(add(field("latitudeDifference"), field("longitudeDifference"))) + // Inaccurate for large distances or close to poles + .as("approximateDistanceToGoogle")) + .execute() + .get(); + // [END sqrt_function] + System.out.println(result.getResults()); + } + + void expFunction() throws ExecutionException, InterruptedException { + // [START exp_function] + Pipeline.Snapshot result = + firestore + .pipeline() + .collection("books") + .select(exp(field("rating")).as("expRating")) + .execute() + .get(); + // [END exp_function] + System.out.println(result.getResults()); + } + + void lnFunction() throws ExecutionException, InterruptedException { + // [START ln_function] + Pipeline.Snapshot result = + firestore + .pipeline() + .collection("books") + .select(ln(field("rating")).as("lnRating")) + .execute() + .get(); + // [END ln_function] + System.out.println(result.getResults()); + } + + void logFunction() throws ExecutionException, InterruptedException { + // [START log_function] + Pipeline.Snapshot result = + firestore + .pipeline() + .collection("books") + .select(log(field("rating"), 2).as("log2Rating")) + .execute() + .get(); + // [END log_function] + System.out.println(result.getResults()); + } + + void arrayConcatFunction() throws ExecutionException, InterruptedException { + // [START array_concat] + Pipeline.Snapshot result = + firestore + .pipeline() + .collection("books") + .select(arrayConcat(field("genre"), field("subGenre")).as("allGenres")) + .execute() + .get(); + // [END array_concat] + System.out.println(result.getResults()); + } + + void arrayContainsFunction() throws ExecutionException, InterruptedException { + // [START array_contains] + Pipeline.Snapshot result = + firestore + .pipeline() + .collection("books") + .select(arrayContains(field("genre"), "mystery").as("isMystery")) + .execute() + .get(); + // [END array_contains] + System.out.println(result.getResults()); + } + + void arrayContainsAllFunction() throws ExecutionException, InterruptedException { + // [START array_contains_all] + Pipeline.Snapshot result = + firestore + .pipeline() + .collection("books") + .select( + arrayContainsAll(field("genre"), Arrays.asList("fantasy", "adventure")) + .as("isFantasyAdventure")) + .execute() + .get(); + // [END array_contains_all] + System.out.println(result.getResults()); + } + + void arrayContainsAnyFunction() throws ExecutionException, InterruptedException { + // [START array_contains_any] + Pipeline.Snapshot result = + firestore + .pipeline() + .collection("books") + .select( + arrayContainsAny(field("genre"), Arrays.asList("fantasy", "nonfiction")) + .as("isMysteryOrFantasy")) + .execute() + .get(); + // [END array_contains_any] + System.out.println(result.getResults()); + } + + void arrayLengthFunction() throws ExecutionException, InterruptedException { + // [START array_length] + Pipeline.Snapshot result = + firestore + .pipeline() + .collection("books") + .select(arrayLength(field("genre")).as("genreCount")) + .execute() + .get(); + // [END array_length] + System.out.println(result.getResults()); + } + + void arrayReverseFunction() throws ExecutionException, InterruptedException { + // [START array_reverse] + Pipeline.Snapshot result = + firestore + .pipeline() + .collection("books") + .select(arrayReverse(field("genre")).as("reversedGenres")) + .execute() + .get(); + // [END array_reverse] + System.out.println(result.getResults()); + } + + void equalFunction() throws ExecutionException, InterruptedException { + // [START equal_function] + Pipeline.Snapshot result = + firestore + .pipeline() + .collection("books") + .select(equal(field("rating"), 5).as("hasPerfectRating")) + .execute() + .get(); + // [END equal_function] + System.out.println(result.getResults()); + } + + void greaterThanFunction() throws ExecutionException, InterruptedException { + // [START greater_than] + Pipeline.Snapshot result = + firestore + .pipeline() + .collection("books") + .select(greaterThan(field("rating"), 4).as("hasHighRating")) + .execute() + .get(); + // [END greater_than] + System.out.println(result.getResults()); + } + + void greaterThanOrEqualToFunction() throws ExecutionException, InterruptedException { + // [START greater_or_equal] + Pipeline.Snapshot result = + firestore + .pipeline() + .collection("books") + .select(greaterThanOrEqual(field("published"), 1900).as("publishedIn20thCentury")) + .execute() + .get(); + // [END greater_or_equal] + System.out.println(result.getResults()); + } + + void lessThanFunction() throws ExecutionException, InterruptedException { + // [START less_than] + Pipeline.Snapshot result = + firestore + .pipeline() + .collection("books") + .select(lessThan(field("published"), 1923).as("isPublicDomainProbably")) + .execute() + .get(); + // [END less_than] + System.out.println(result.getResults()); + } + + void lessThanOrEqualToFunction() throws ExecutionException, InterruptedException { + // [START less_or_equal] + Pipeline.Snapshot result = + firestore + .pipeline() + .collection("books") + .select(lessThanOrEqual(field("rating"), 2).as("hasBadRating")) + .execute() + .get(); + // [END less_or_equal] + System.out.println(result.getResults()); + } + + void notEqualFunction() throws ExecutionException, InterruptedException { + // [START not_equal] + Pipeline.Snapshot result = + firestore + .pipeline() + .collection("books") + .select(notEqual(field("title"), "1984").as("not1984")) + .execute() + .get(); + // [END not_equal] + System.out.println(result.getResults()); + } + + void existsFunction() throws ExecutionException, InterruptedException { + // [START exists_function] + Pipeline.Snapshot result = + firestore + .pipeline() + .collection("books") + .select(exists(field("rating")).as("hasRating")) + .execute() + .get(); + // [END exists_function] + System.out.println(result.getResults()); + } + + void andFunction() throws ExecutionException, InterruptedException { + // [START and_function] + Pipeline.Snapshot result = + firestore + .pipeline() + .collection("books") + .select( + and(greaterThan(field("rating"), 4), lessThan(field("price"), 10)) + .as("under10Recommendation")) + .execute() + .get(); + // [END and_function] + System.out.println(result.getResults()); + } + + void orFunction() throws ExecutionException, InterruptedException { + // [START or_function] + Pipeline.Snapshot result = + firestore + .pipeline() + .collection("books") + .select( + or(equal(field("genre"), "Fantasy"), arrayContains(field("tags"), "adventure")) + .as("matchesSearchFilters")) + .execute() + .get(); + // [END or_function] + System.out.println(result.getResults()); + } + + void xorFunction() throws ExecutionException, InterruptedException { + // [START xor_function] + Pipeline.Snapshot result = + firestore + .pipeline() + .collection("books") + .select( + xor( + arrayContains(field("tags"), "magic"), + arrayContains(field("tags"), "nonfiction")) + .as("matchesSearchFilters")) + .execute() + .get(); + // [END xor_function] + System.out.println(result.getResults()); + } + + void notFunction() throws ExecutionException, InterruptedException { + // [START not_function] + Pipeline.Snapshot result = + firestore + .pipeline() + .collection("books") + .select(not(arrayContains(field("tags"), "nonfiction")).as("isFiction")) + .execute() + .get(); + // [END not_function] + System.out.println(result.getResults()); + } + + void condFunction() throws ExecutionException, InterruptedException { + // [START cond_function] + Pipeline.Snapshot result = + firestore + .pipeline() + .collection("books") + .select( + arrayConcat( + field("tags"), + conditional( + greaterThan(field("pages"), 100), + constant("longRead"), + constant("shortRead"))) + .as("extendedTags")) + .execute() + .get(); + // [END cond_function] + System.out.println(result.getResults()); + } + + void equalAnyFunction() throws ExecutionException, InterruptedException { + // [START eq_any] + Pipeline.Snapshot result = + firestore + .pipeline() + .collection("books") + .select( + equalAny(field("genre"), Arrays.asList("Science Fiction", "Psychological Thriller")) + .as("matchesGenreFilters")) + .execute() + .get(); + // [END eq_any] + System.out.println(result.getResults()); + } + + void notEqualAnyFunction() throws ExecutionException, InterruptedException { + // [START not_eq_any] + Pipeline.Snapshot result = + firestore + .pipeline() + .collection("books") + .select( + notEqualAny(field("author"), Arrays.asList("George Orwell", "F. Scott Fitzgerald")) + .as("byExcludedAuthors")) + .execute() + .get(); + // [END not_eq_any] + System.out.println(result.getResults()); + } + + void maxLogicalFunction() throws ExecutionException, InterruptedException { + // [START max_logical_function] + Pipeline.Snapshot result = + firestore + .pipeline() + .collection("books") + .select(logicalMaximum(field("rating"), 1).as("flooredRating")) + .execute() + .get(); + // [END max_logical_function] + System.out.println(result.getResults()); + } + + void minLogicalFunction() throws ExecutionException, InterruptedException { + // [START min_logical_function] + Pipeline.Snapshot result = + firestore + .pipeline() + .collection("books") + .select(logicalMinimum(field("rating"), 5).as("cappedRating")) + .execute() + .get(); + // [END min_logical_function] + System.out.println(result.getResults()); + } + + void mapGetFunction() throws ExecutionException, InterruptedException { + // [START map_get] + Pipeline.Snapshot result = + firestore + .pipeline() + .collection("books") + .select(mapGet(field("awards"), "pulitzer").as("hasPulitzerAward")) + .execute() + .get(); + // [END map_get] + System.out.println(result.getResults()); + } + + void byteLengthFunction() throws ExecutionException, InterruptedException { + // [START byte_length] + Pipeline.Snapshot result = + firestore + .pipeline() + .collection("books") + .select(byteLength(field("title")).as("titleByteLength")) + .execute() + .get(); + // [END byte_length] + System.out.println(result.getResults()); + } + + void charLengthFunction() throws ExecutionException, InterruptedException { + // [START char_length] + Pipeline.Snapshot result = + firestore + .pipeline() + .collection("books") + .select(charLength(field("title")).as("titleCharLength")) + .execute() + .get(); + // [END char_length] + System.out.println(result.getResults()); + } + + void startsWithFunction() throws ExecutionException, InterruptedException { + // [START starts_with] + Pipeline.Snapshot result = + firestore + .pipeline() + .collection("books") + .select(startsWith(field("title"), "The").as("needsSpecialAlphabeticalSort")) + .execute() + .get(); + // [END starts_with] + System.out.println(result.getResults()); + } + + void endsWithFunction() throws ExecutionException, InterruptedException { + // [START ends_with] + Pipeline.Snapshot result = + firestore + .pipeline() + .collection("inventory/devices/laptops") + .select(endsWith(field("name"), "16 inch").as("16InLaptops")) + .execute() + .get(); + // [END ends_with] + System.out.println(result.getResults()); + } + + void likeFunction() throws ExecutionException, InterruptedException { + // [START like] + Pipeline.Snapshot result = + firestore + .pipeline() + .collection("books") + .select(like(field("genre"), "%Fiction").as("anyFiction")) + .execute() + .get(); + // [END like] + System.out.println(result.getResults()); + } + + void regexContainsFunction() throws ExecutionException, InterruptedException { + // [START regex_contains] + Pipeline.Snapshot result = + firestore + .pipeline() + .collection("documents") + .select( + regexContains(field("title"), "Firestore (Enterprise|Standard)") + .as("isFirestoreRelated")) + .execute() + .get(); + // [END regex_contains] + System.out.println(result.getResults()); + } + + void regexMatchFunction() throws ExecutionException, InterruptedException { + // [START regex_match] + Pipeline.Snapshot result = + firestore + .pipeline() + .collection("documents") + .select( + regexMatch(field("title"), "Firestore (Enterprise|Standard)") + .as("isFirestoreExactly")) + .execute() + .get(); + // [END regex_match] + System.out.println(result.getResults()); + } + + void strConcatFunction() throws ExecutionException, InterruptedException { + // [START str_concat] + Pipeline.Snapshot result = + firestore + .pipeline() + .collection("books") + .select(stringConcat(field("title"), " by ", field("author")).as("fullyQualifiedTitle")) + .execute() + .get(); + // [END str_concat] + System.out.println(result.getResults()); + } + + void strContainsFunction() throws ExecutionException, InterruptedException { + // [START string_contains] + Pipeline.Snapshot result = + firestore + .pipeline() + .collection("articles") + .select(stringContains(field("body"), "Firestore").as("isFirestoreRelated")) + .execute() + .get(); + // [END string_contains] + System.out.println(result.getResults()); + } + + void toUpperFunction() throws ExecutionException, InterruptedException { + // [START to_upper] + Pipeline.Snapshot result = + firestore + .pipeline() + .collection("authors") + .select(toUpper(field("name")).as("uppercaseName")) + .execute() + .get(); + // [END to_upper] + System.out.println(result.getResults()); + } + + void toLowerFunction() throws ExecutionException, InterruptedException { + // [START to_lower] + Pipeline.Snapshot result = + firestore + .pipeline() + .collection("authors") + .select(equal(toLower(field("genre")), "fantasy").as("isFantasy")) + .execute() + .get(); + // [END to_lower] + System.out.println(result.getResults()); + } + + void substrFunction() throws ExecutionException, InterruptedException { + // [START substr_function] + Pipeline.Snapshot result = + firestore + .pipeline() + .collection("books") + .where(startsWith(field("title"), "The ")) + .select( + substring(field("title"), constant(4), field("title").charLength()) + .as("titleWithoutLeadingThe")) + .execute() + .get(); + // [END substr_function] + System.out.println(result.getResults()); + } + + void strReverseFunction() throws ExecutionException, InterruptedException { + // [START str_reverse] + Pipeline.Snapshot result = + firestore + .pipeline() + .collection("books") + .select(reverse(field("name")).as("reversedName")) + .execute() + .get(); + // [END str_reverse] + System.out.println(result.getResults()); + } + + void strTrimFunction() throws ExecutionException, InterruptedException { + // [START trim_function] + Pipeline.Snapshot result = + firestore + .pipeline() + .collection("books") + .select(trim(field("name")).as("whitespaceTrimmedName")) + .execute() + .get(); + // [END trim_function] + System.out.println(result.getResults()); + } + + void unixMicrosToTimestampFunction() throws ExecutionException, InterruptedException { + // [START unix_micros_timestamp] + Pipeline.Snapshot result = + firestore + .pipeline() + .collection("documents") + .select(unixMicrosToTimestamp(field("createdAtMicros")).as("createdAtString")) + .execute() + .get(); + // [END unix_micros_timestamp] + System.out.println(result.getResults()); + } + + void unixMillisToTimestampFunction() throws ExecutionException, InterruptedException { + // [START unix_millis_timestamp] + Pipeline.Snapshot result = + firestore + .pipeline() + .collection("documents") + .select(unixMillisToTimestamp(field("createdAtMillis")).as("createdAtString")) + .execute() + .get(); + // [END unix_millis_timestamp] + System.out.println(result.getResults()); + } + + void unixSecondsToTimestampFunction() throws ExecutionException, InterruptedException { + // [START unix_seconds_timestamp] + Pipeline.Snapshot result = + firestore + .pipeline() + .collection("documents") + .select(unixSecondsToTimestamp(field("createdAtSeconds")).as("createdAtString")) + .execute() + .get(); + // [END unix_seconds_timestamp] + System.out.println(result.getResults()); + } + + void timestampAddFunction() throws ExecutionException, InterruptedException { + // [START timestamp_add] + Pipeline.Snapshot result = + firestore + .pipeline() + .collection("documents") + .select(timestampAdd(field("createdAt"), "day", 3653).as("expiresAt")) + .execute() + .get(); + // [END timestamp_add] + System.out.println(result.getResults()); + } + + void timestampSubFunction() throws ExecutionException, InterruptedException { + // [START timestamp_sub] + Pipeline.Snapshot result = + firestore + .pipeline() + .collection("documents") + .select(timestampSubtract(field("expiresAt"), "day", 14).as("sendWarningTimestamp")) + .execute() + .get(); + // [END timestamp_sub] + System.out.println(result.getResults()); + } + + void timestampToUnixMicrosFunction() throws ExecutionException, InterruptedException { + // [START timestamp_unix_micros] + Pipeline.Snapshot result = + firestore + .pipeline() + .collection("documents") + .select(timestampToUnixMicros(field("dateString")).as("unixMicros")) + .execute() + .get(); + // [END timestamp_unix_micros] + System.out.println(result.getResults()); + } + + void timestampToUnixMillisFunction() throws ExecutionException, InterruptedException { + // [START timestamp_unix_millis] + Pipeline.Snapshot result = + firestore + .pipeline() + .collection("documents") + .select(timestampToUnixMillis(field("dateString")).as("unixMillis")) + .execute() + .get(); + // [END timestamp_unix_millis] + System.out.println(result.getResults()); + } + + void timestampToUnixSecondsFunction() throws ExecutionException, InterruptedException { + // [START timestamp_unix_seconds] + Pipeline.Snapshot result = + firestore + .pipeline() + .collection("documents") + .select(timestampToUnixSeconds(field("dateString")).as("unixSeconds")) + .execute() + .get(); + // [END timestamp_unix_seconds] + System.out.println(result.getResults()); + } + + void cosineDistanceFunction() throws ExecutionException, InterruptedException { + // [START cosine_distance] + double[] sampleVector = new double[] {0.0, 1.0, 2.0, 3.0, 4.0, 5.0}; + Pipeline.Snapshot result = + firestore + .pipeline() + .collection("books") + .select(cosineDistance(field("embedding"), sampleVector).as("cosineDistance")) + .execute() + .get(); + // [END cosine_distance] + System.out.println(result.getResults()); + } + + void dotProductFunction() throws ExecutionException, InterruptedException { + // [START dot_product] + double[] sampleVector = new double[] {0.0, 1.0, 2.0, 3.0, 4.0, 5.0}; + Pipeline.Snapshot result = + firestore + .pipeline() + .collection("books") + .select(dotProduct(field("embedding"), sampleVector).as("dotProduct")) + .execute() + .get(); + // [END dot_product] + System.out.println(result.getResults()); + } + + void euclideanDistanceFunction() throws ExecutionException, InterruptedException { + // [START euclidean_distance] + double[] sampleVector = new double[] {0.0, 1.0, 2.0, 3.0, 4.0, 5.0}; + Pipeline.Snapshot result = + firestore + .pipeline() + .collection("books") + .select(euclideanDistance(field("embedding"), sampleVector).as("euclideanDistance")) + .execute() + .get(); + // [END euclidean_distance] + System.out.println(result.getResults()); + } + + void vectorLengthFunction() throws ExecutionException, InterruptedException { + // [START vector_length] + Pipeline.Snapshot result = + firestore + .pipeline() + .collection("books") + .select(vectorLength(field("embedding")).as("vectorLength")) + .execute() + .get(); + // [END vector_length] + System.out.println(result.getResults()); + } + + // https://cloud.google.com/firestore/docs/pipeline/getting-started/stages-expressions + void stagesExpressionsExample() throws ExecutionException, InterruptedException { + // [START stages_expressions_example] + com.google.cloud.firestore.pipeline.expressions.Expression trailing30Days = + constant(com.google.cloud.Timestamp.now().toProto().getSeconds() * 1000) + .unixMillisToTimestamp() + .timestampSubtract("day", 30); + Pipeline.Snapshot snapshot = + firestore + .pipeline() + .collection("productViews") + .where(field("viewedAt").greaterThan(trailing30Days)) + .aggregate(countDistinct("productId").as("uniqueProductViews")) + .execute() + .get(); + // [END stages_expressions_example] + System.out.println(snapshot.getResults()); + } + + // https://cloud.google.com/firestore/docs/pipeline/stages/transformation/where + void createWhereData() { + // [START create_where_data] + firestore + .collection("cities") + .document("SF") + .set( + new HashMap() { + { + put("name", "San Francisco"); + put("state", "CA"); + put("country", "USA"); + put("population", 870000); + } + }); + firestore + .collection("cities") + .document("LA") + .set( + new HashMap() { + { + put("name", "Los Angeles"); + put("state", "CA"); + put("country", "USA"); + put("population", 3970000); + } + }); + firestore + .collection("cities") + .document("NY") + .set( + new HashMap() { + { + put("name", "New York"); + put("state", "NY"); + put("country", "USA"); + put("population", 8530000); + } + }); + firestore + .collection("cities") + .document("TOR") + .set( + new HashMap() { + { + put("name", "Toronto"); + put("state", null); + put("country", "Canada"); + put("population", 2930000); + } + }); + firestore + .collection("cities") + .document("MEX") + .set( + new HashMap() { + { + put("name", "Mexico City"); + put("state", null); + put("country", "Mexico"); + put("population", 9200000); + } + }); + // [END create_where_data] + } + + void whereEqualityExample() throws ExecutionException, InterruptedException { + // [START where_equality_example] + Pipeline.Snapshot cities = + firestore.pipeline().collection("cities").where(field("state").equal("CA")).execute().get(); + // [END where_equality_example] + System.out.println(cities.getResults()); + } + + void whereMultipleStagesExample() throws ExecutionException, InterruptedException { + // [START where_multiple_stages] + Pipeline.Snapshot cities = + firestore + .pipeline() + .collection("cities") + .where(field("location.country").equal("USA")) + .where(field("population").greaterThan(500000)) + .execute() + .get(); + // [END where_multiple_stages] + System.out.println(cities.getResults()); + } + + void whereComplexExample() throws ExecutionException, InterruptedException { + // [START where_complex] + Pipeline.Snapshot cities = + firestore + .pipeline() + .collection("cities") + .where( + or( + like(field("name"), "San%"), + and( + field("location.state").charLength().greaterThan(7), + field("location.country").equal("USA")))) + .execute() + .get(); + // [END where_complex] + System.out.println(cities.getResults()); + } + + void whereStageOrderExample() throws ExecutionException, InterruptedException { + // [START where_stage_order] + Pipeline.Snapshot cities = + firestore + .pipeline() + .collection("cities") + .limit(10) + .where(field("location.country").equal("USA")) + .execute() + .get(); + // [END where_stage_order] + System.out.println(cities.getResults()); + } + + void whereHavingExample() throws ExecutionException, InterruptedException { + // [START where_having_example] + Pipeline.Snapshot cities = + firestore + .pipeline() + .collection("cities") + .aggregate( + Aggregate.withAccumulators(sum("population").as("totalPopulation")) + .withGroups("location.state")) + .where(field("totalPopulation").greaterThan(10000000)) + .execute() + .get(); + // [END where_having_example] + System.out.println(cities.getResults()); + } + + // https://cloud.google.com/firestore/docs/pipeline/stages/transformation/unnest + void unnestSyntaxExample() throws ExecutionException, InterruptedException { + // [START unnest_syntax] + Pipeline.Snapshot userScore = + firestore + .pipeline() + .collection("users") + .unnest("scores", "userScore", new UnnestOptions().withIndexField("attempt")) + .execute() + .get(); + // [END unnest_syntax] + System.out.println(userScore.getResults()); + } + + void unnestAliasIndexDataExample() { + // [START unnest_alias_index_data] + firestore + .collection("users") + .add( + new HashMap() { + { + put("name", "foo"); + put("scores", Arrays.asList(5, 4)); + put("userScore", 0); + } + }); + firestore + .collection("users") + .add( + new HashMap() { + { + put("name", "bar"); + put("scores", Arrays.asList(1, 3)); + put("attempt", 5); + } + }); + // [END unnest_alias_index_data] + } + + void unnestAliasIndexExample() throws ExecutionException, InterruptedException { + // [START unnest_alias_index] + Pipeline.Snapshot userScore = + firestore + .pipeline() + .collection("users") + .unnest("scores", "userScore", new UnnestOptions().withIndexField("attempt")) + .execute() + .get(); + // [END unnest_alias_index] + System.out.println(userScore.getResults()); + } + + void unnestNonArrayDataExample() { + // [START unnest_nonarray_data] + firestore + .collection("users") + .add( + new HashMap() { + { + put("name", "foo"); + put("scores", 1); + } + }); + firestore + .collection("users") + .add( + new HashMap() { + { + put("name", "bar"); + put("scores", null); + } + }); + firestore + .collection("users") + .add( + new HashMap() { + { + put("name", "qux"); + put( + "scores", + new HashMap() { + { + put("backupScores", 1); + } + }); + } + }); + // [END unnest_nonarray_data] + } + + void unnestNonArrayExample() throws ExecutionException, InterruptedException { + // [START unnest_nonarray] + Pipeline.Snapshot userScore = + firestore + .pipeline() + .collection("users") + .unnest("scores", "userScore", new UnnestOptions().withIndexField("attempt")) + .execute() + .get(); + // [END unnest_nonarray] + System.out.println(userScore.getResults()); + } + + void unnestEmptyArrayDataExample() { + // [START unnest_empty_array_data] + firestore + .collection("users") + .add( + new HashMap() { + { + put("name", "foo"); + put("scores", Arrays.asList(5, 4)); + } + }); + firestore + .collection("users") + .add( + new HashMap() { + { + put("name", "bar"); + put("scores", Arrays.asList()); + } + }); + // [END unnest_empty_array_data] + } + + void unnestEmptyArrayExample() throws ExecutionException, InterruptedException { + // [START unnest_empty_array] + Pipeline.Snapshot userScore = + firestore + .pipeline() + .collection("users") + .unnest("scores", "userScore", new UnnestOptions().withIndexField("attempt")) + .execute() + .get(); + // [END unnest_empty_array] + System.out.println(userScore.getResults()); + } + + void unnestPreserveEmptyArrayExample() throws ExecutionException, InterruptedException { + // [START unnest_preserve_empty_array] + // Seems like the unnest method is missing: + // https://github.com/googleapis/java-firestore/blob/742fab6583c9a6f9c47cf0496124c3c9b05fe0ee/google-cloud-firestore/src/main/java/com/google/cloud/firestore/Pipeline.java#L995 + // Pipeline.Snapshot userScore = + // firestore + // .pipeline() + // .collection("users") + // .unnest( + // conditional( + // field("scores").equal(array()), + // array(field("scores")), + // field("scores") + // ).as("userScore"), + // new UnnestOptions().withIndexField("attempt")) + // .execute() + // .get(); + // [END unnest_preserve_empty_array] + // System.out.println(userScore.getResults()); + } + + void unnestNestedDataExample() { + // [START unnest_nested_data] + firestore + .collection("users") + .add( + new HashMap() { + { + put("name", "foo"); + put( + "record", + Arrays.asList( + new HashMap() { + { + put("scores", Arrays.asList(5, 4)); + put("avg", 4.5); + } + }, + new HashMap() { + { + put("scores", Arrays.asList(1, 3)); + put("old_avg", 2); + } + })); + } + }); + // [END unnest_nested_data] + } + + void unnestNestedExample() throws ExecutionException, InterruptedException { + // [START unnest_nested] + Pipeline.Snapshot userScore = + firestore + .pipeline() + .collection("users") + .unnest("record", "record") + .unnest("record.scores", "userScore", new UnnestOptions().withIndexField("attempt")) + .execute() + .get(); + // [END unnest_nested] + System.out.println(userScore.getResults()); + } + + // https://cloud.corp.google.com/firestore/docs/pipeline/stages/transformation/sample + void sampleSyntaxExample() throws ExecutionException, InterruptedException { + // [START sample_syntax] + Pipeline.Snapshot sampled1 = firestore.pipeline().database().sample(50).execute().get(); + + Pipeline.Snapshot sampled2 = + firestore.pipeline().database().sample(Sample.withPercentage(0.5)).execute().get(); + // [END sample_syntax] + System.out.println(sampled1.getResults()); + System.out.println(sampled2.getResults()); + } + + void sampleDocumentsDataExample() { + // [START sample_documents_data] + firestore + .collection("cities") + .document("SF") + .set( + new HashMap() { + { + put("name", "San Francisco"); + put("state", "California"); + } + }); + firestore + .collection("cities") + .document("NYC") + .set( + new HashMap() { + { + put("name", "New York City"); + put("state", "New York"); + } + }); + firestore + .collection("cities") + .document("CHI") + .set( + new HashMap() { + { + put("name", "Chicago"); + put("state", "Illinois"); + } + }); + // [END sample_documents_data] + } + + void sampleDocumentsExample() throws ExecutionException, InterruptedException { + // [START sample_documents] + Pipeline.Snapshot sampled = firestore.pipeline().collection("cities").sample(1).execute().get(); + // [END sample_documents] + System.out.println(sampled.getResults()); + } + + void sampleAllDocumentsExample() throws ExecutionException, InterruptedException { + // [START sample_all_documents] + Pipeline.Snapshot sampled = firestore.pipeline().collection("cities").sample(5).execute().get(); + // [END sample_all_documents] + System.out.println(sampled.getResults()); + } + + void samplePercentageDataExample() { + // [START sample_percentage_data] + firestore + .collection("cities") + .document("SF") + .set( + new HashMap() { + { + put("name", "San Francisco"); + put("state", "California"); + } + }); + firestore + .collection("cities") + .document("NYC") + .set( + new HashMap() { + { + put("name", "New York City"); + put("state", "New York"); + } + }); + firestore + .collection("cities") + .document("CHI") + .set( + new HashMap() { + { + put("name", "Chicago"); + put("state", "Illinois"); + } + }); + firestore + .collection("cities") + .document("ATL") + .set( + new HashMap() { + { + put("name", "Atlanta"); + put("state", "Georgia"); + } + }); + // [END sample_percentage_data] + } + + void samplePercentageExample() throws ExecutionException, InterruptedException { + // [START sample_percentage] + Pipeline.Snapshot sampled = + firestore + .pipeline() + .collection("cities") + .sample(Sample.withPercentage(0.5)) + .execute() + .get(); + // [END sample_percentage] + System.out.println(sampled.getResults()); + } + + // https://cloud.google.com/firestore/docs/pipeline/stages/transformation/sort + void sortSyntaxExample() throws ExecutionException, InterruptedException { + // [START sort_syntax] + Pipeline.Snapshot results = + firestore + .pipeline() + .collection("cities") + .sort(ascending(field("population"))) + .execute() + .get(); + // [END sort_syntax] + System.out.println(results.getResults()); + } + + void sortSyntaxExample2() throws ExecutionException, InterruptedException { + // [START sort_syntax_2] + Pipeline.Snapshot results = + firestore + .pipeline() + .collection("cities") + .sort(ascending(charLength(field("name")))) + .execute() + .get(); + // [END sort_syntax_2] + System.out.println(results.getResults()); + } + + void sortDocumentIDExample() throws ExecutionException, InterruptedException { + // [START sort_document_id] + Pipeline.Snapshot results = + firestore + .pipeline() + .collection("cities") + .sort(ascending(field("country")), ascending(field("__name__"))) + .execute() + .get(); + // [END sort_document_id] + System.out.println(results.getResults()); + } + + // https://cloud.google.com/firestore/docs/pipeline/stages/transformation/select + void selectSyntaxExample() throws ExecutionException, InterruptedException { + // [START select_syntax] + Pipeline.Snapshot names = + firestore + .pipeline() + .collection("cities") + .select( + stringConcat(field("name"), ", ", field("location.country")).as("name"), + field("population")) + .execute() + .get(); + // [END select_syntax] + System.out.println(names.getResults()); + } + + void selectPositionDataExample() { + // [START select_position_data] + firestore + .collection("cities") + .document("SF") + .set( + new HashMap() { + { + put("name", "San Francisco"); + put("population", 800000); + put( + "location", + new HashMap() { + { + put("country", "USA"); + put("state", "California"); + } + }); + } + }); + firestore + .collection("cities") + .document("TO") + .set( + new HashMap() { + { + put("name", "Toronto"); + put("population", 3000000); + put( + "location", + new HashMap() { + { + put("country", "Canada"); + put("province", "Ontario"); + } + }); + } + }); + // [END select_position_data] + } + + void selectPositionExample() throws ExecutionException, InterruptedException { + // [START select_position] + Pipeline.Snapshot names = + firestore + .pipeline() + .collection("cities") + .where(field("location.country").equal("Canada")) + .select( + stringConcat(field("name"), ", ", field("location.country")).as("name"), + field("population")) + .execute() + .get(); + // [END select_position] + System.out.println(names.getResults()); + } + + void selectBadPositionExample() throws ExecutionException, InterruptedException { + // [START select_bad_position] + Pipeline.Snapshot names = + firestore + .pipeline() + .collection("cities") + .select( + stringConcat(field("name"), ", ", field("location.country")).as("name"), + field("population")) + .where(field("location.country").equal("Canada")) + .execute() + .get(); + // [END select_bad_position] + System.out.println(names.getResults()); + } + + void selectNestedDataExample() { + // [START select_nested_data] + firestore + .collection("cities") + .document("SF") + .set( + new HashMap() { + { + put("name", "San Francisco"); + put("population", 800000); + put( + "location", + new HashMap() { + { + put("country", "USA"); + put("state", "California"); + } + }); + put("landmarks", Arrays.asList("Golden Gate Bridge", "Alcatraz")); + } + }); + firestore + .collection("cities") + .document("TO") + .set( + new HashMap() { + { + put("name", "Toronto"); + put("population", 3000000); + put("province", "ON"); + put( + "location", + new HashMap() { + { + put("country", "Canada"); + put("province", "Ontario"); + } + }); + put("landmarks", Arrays.asList("CN Tower", "Casa Loma")); + } + }); + firestore + .collection("cities") + .document("AT") + .set( + new HashMap() { + { + put("name", "Atlantis"); + put("population", null); + } + }); + // [END select_nested_data] + } + + void selectNestedExample() throws ExecutionException, InterruptedException { + // [START select_nested] + Pipeline.Snapshot locations = + firestore + .pipeline() + .collection("cities") + .select( + field("name").as("city"), + field("location.country").as("country"), + arrayGet(field("landmarks"), 0).as("topLandmark")) + .execute() + .get(); + // [END select_nested] + System.out.println(locations.getResults()); + } + + // https://cloud.google.com/firestore/docs/pipeline/stages/transformation/remove_fields + void removeFieldsSyntaxExample() throws ExecutionException, InterruptedException { + // [START remove_fields_syntax] + Pipeline.Snapshot results = + firestore + .pipeline() + .collection("cities") + .removeFields("population", "location.state") + .execute() + .get(); + // [END remove_fields_syntax] + System.out.println(results.getResults()); + } + + void removeFieldsNestedDataExample() { + // [START remove_fields_nested_data] + firestore + .collection("cities") + .document("SF") + .set( + new HashMap() { + { + put("name", "San Francisco"); + put( + "location", + new HashMap() { + { + put("country", "USA"); + put("state", "California"); + } + }); + } + }); + firestore + .collection("cities") + .document("TO") + .set( + new HashMap() { + { + put("name", "Toronto"); + put( + "location", + new HashMap() { + { + put("country", "Canada"); + put("province", "Ontario"); + } + }); + } + }); + // [END remove_fields_nested_data] + } + + void removeFieldsNestedExample() throws ExecutionException, InterruptedException { + // [START remove_fields_nested] + Pipeline.Snapshot results = + firestore.pipeline().collection("cities").removeFields("location.state").execute().get(); + // [END remove_fields_nested] + System.out.println(results.getResults()); + } + + // https://cloud.google.com/firestore/docs/pipeline/stages/transformation/limit + void limitSyntaxExample() throws ExecutionException, InterruptedException { + // [START limit_syntax] + Pipeline.Snapshot results = firestore.pipeline().collection("cities").limit(10).execute().get(); + // [END limit_syntax] + System.out.println(results.getResults()); + } + + // https://cloud.google.com/firestore/docs/pipeline/stages/transformation/find_nearest + void findNearestSyntaxExample() throws ExecutionException, InterruptedException { + // [START find_nearest_syntax] + Pipeline.Snapshot results = + firestore + .pipeline() + .collection("cities") + .findNearest( + "embedding", + new double[] {1.5, 2.345}, + FindNearest.DistanceMeasure.EUCLIDEAN, + new FindNearestOptions()) + .execute() + .get(); + // [END find_nearest_syntax] + System.out.println(results.getResults()); + } + + void findNearestLimitExample() throws ExecutionException, InterruptedException { + // [START find_nearest_limit] + Pipeline.Snapshot results = + firestore + .pipeline() + .collection("cities") + .findNearest( + "embedding", + new double[] {1.5, 2.345}, + FindNearest.DistanceMeasure.EUCLIDEAN, + new FindNearestOptions().withLimit(10)) + .execute() + .get(); + // [END find_nearest_limit] + System.out.println(results.getResults()); + } + + void findNearestDistanceDataExample() { + // [START find_nearest_distance_data] + firestore + .collection("cities") + .document("SF") + .set( + new HashMap() { + { + put("name", "San Francisco"); + put("embedding", Arrays.asList(1.0, -1.0)); + } + }); + firestore + .collection("cities") + .document("TO") + .set( + new HashMap() { + { + put("name", "Toronto"); + put("embedding", Arrays.asList(5.0, -10.0)); + } + }); + firestore + .collection("cities") + .document("AT") + .set( + new HashMap() { + { + put("name", "Atlantis"); + put("embedding", Arrays.asList(2.0, -4.0)); + } + }); + // [END find_nearest_distance_data] + } + + void findNearestDistanceExample() throws ExecutionException, InterruptedException { + // [START find_nearest_distance] + Pipeline.Snapshot results = + firestore + .pipeline() + .collection("cities") + .findNearest( + "embedding", + new double[] {1.3, 2.345}, + FindNearest.DistanceMeasure.EUCLIDEAN, + new FindNearestOptions().withDistanceField("computedDistance")) + .execute() + .get(); + // [END find_nearest_distance] + System.out.println(results.getResults()); + } + + // https://cloud.google.com/firestore/docs/pipeline/stages/transformation/offset + void offsetSyntaxExample() throws ExecutionException, InterruptedException { + // [START offset_syntax] + Pipeline.Snapshot results = + firestore.pipeline().collection("cities").offset(10).execute().get(); + // [END offset_syntax] + System.out.println(results.getResults()); + } + + // https://cloud.google.com/firestore/docs/pipeline/stages/transformation/add_fields + void addFieldsSyntaxExample() throws ExecutionException, InterruptedException { + // [START add_fields_syntax] + Pipeline.Snapshot results = + firestore + .pipeline() + .collection("users") + .addFields(stringConcat(field("firstName"), " ", field("lastName")).as("fullName")) + .execute() + .get(); + // [END add_fields_syntax] + System.out.println(results.getResults()); + } + + void addFieldsOverlapExample() throws ExecutionException, InterruptedException { + // [START add_fields_overlap] + Pipeline.Snapshot results = + firestore + .pipeline() + .collection("users") + .addFields(abs(field("age")).as("age")) + .addFields(add(field("age"), 10).as("age")) + .execute() + .get(); + // [END add_fields_overlap] + System.out.println(results.getResults()); + } + + void addFieldsNestingExample() throws ExecutionException, InterruptedException { + // [START add_fields_nesting] + Pipeline.Snapshot results = + firestore + .pipeline() + .collection("users") + .addFields(toLower(field("address.city")).as("address.city")) + .execute() + .get(); + // [END add_fields_nesting] + System.out.println(results.getResults()); + } + + // https://cloud.google.com/firestore/docs/pipeline/stages/input/collection + void collectionInputSyntaxExample() throws ExecutionException, InterruptedException { + // [START collection_input_syntax] + Pipeline.Snapshot results = + firestore.pipeline().collection("cities/SF/departments").execute().get(); + // [END collection_input_syntax] + System.out.println(results.getResults()); + } + + void collectionInputExampleData() { + // [START collection_input_data] + firestore + .collection("cities") + .document("SF") + .set( + new HashMap() { + { + put("name", "San Francisco"); + put("state", "California"); + } + }); + firestore + .collection("cities") + .document("NYC") + .set( + new HashMap() { + { + put("name", "New York City"); + put("state", "New York"); + } + }); + firestore + .collection("cities") + .document("CHI") + .set( + new HashMap() { + { + put("name", "Chicago"); + put("state", "Illinois"); + } + }); + firestore + .collection("states") + .document("CA") + .set( + new HashMap() { + { + put("name", "California"); + } + }); + // [END collection_input_data] + } + + void collectionInputExample() throws ExecutionException, InterruptedException { + // [START collection_input] + Pipeline.Snapshot results = + firestore.pipeline().collection("cities").sort(ascending(field("name"))).execute().get(); + // [END collection_input] + System.out.println(results.getResults()); + } + + void subcollectionInputExampleData() { + // [START subcollection_input_data] + firestore + .collection("cities/SF/departments") + .document("building") + .set( + new HashMap() { + { + put("name", "SF Building Department"); + put("employees", 750); + } + }); + firestore + .collection("cities/NY/departments") + .document("building") + .set( + new HashMap() { + { + put("name", "NY Building Department"); + put("employees", 1000); + } + }); + firestore + .collection("cities/CHI/departments") + .document("building") + .set( + new HashMap() { + { + put("name", "CHI Building Department"); + put("employees", 900); + } + }); + firestore + .collection("cities/NY/departments") + .document("finance") + .set( + new HashMap() { + { + put("name", "NY Finance Department"); + put("employees", 1200); + } + }); + // [END subcollection_input_data] + } + + void subcollectionInputExample() throws ExecutionException, InterruptedException { + // [START subcollection_input] + Pipeline.Snapshot results = + firestore + .pipeline() + .collection("cities/NY/departments") + .sort(ascending(field("employees"))) + .execute() + .get(); + // [END subcollection_input] + System.out.println(results.getResults()); + } + + // https://cloud.google.com/firestore/docs/pipeline/stages/input/collection_group + void collectionGroupInputSyntaxExample() throws ExecutionException, InterruptedException { + // [START collection_group_input_syntax] + Pipeline.Snapshot results = firestore.pipeline().collectionGroup("departments").execute().get(); + // [END collection_group_input_syntax] + System.out.println(results.getResults()); + } + + void collectionGroupInputExampleData() { + // [START collection_group_data] + firestore + .collection("cities/SF/departments") + .document("building") + .set( + new HashMap() { + { + put("name", "SF Building Department"); + put("employees", 750); + } + }); + firestore + .collection("cities/NY/departments") + .document("building") + .set( + new HashMap() { + { + put("name", "NY Building Department"); + put("employees", 1000); + } + }); + firestore + .collection("cities/CHI/departments") + .document("building") + .set( + new HashMap() { + { + put("name", "CHI Building Department"); + put("employees", 900); + } + }); + firestore + .collection("cities/NY/departments") + .document("finance") + .set( + new HashMap() { + { + put("name", "NY Finance Department"); + put("employees", 1200); + } + }); + // [END collection_group_data] + } + + void collectionGroupInputExample() throws ExecutionException, InterruptedException { + // [START collection_group_input] + Pipeline.Snapshot results = + firestore + .pipeline() + .collectionGroup("departments") + .sort(ascending(field("employees"))) + .execute() + .get(); + // [END collection_group_input] + System.out.println(results.getResults()); + } + + // https://cloud.google.com/firestore/docs/pipeline/stages/input/database + void databaseInputSyntaxExample() throws ExecutionException, InterruptedException { + // [START database_syntax] + Pipeline.Snapshot results = firestore.pipeline().database().execute().get(); + // [END database_syntax] + System.out.println(results.getResults()); + } + + void databaseInputSyntaxExampleData() { + // [START database_input_data] + firestore + .collection("cities") + .document("SF") + .set( + new HashMap() { + { + put("name", "San Francisco"); + put("state", "California"); + put("population", 800000); + } + }); + firestore + .collection("states") + .document("CA") + .set( + new HashMap() { + { + put("name", "California"); + put("population", 39000000); + } + }); + firestore + .collection("countries") + .document("USA") + .set( + new HashMap() { + { + put("name", "United States of America"); + put("population", 340000000); + } + }); + // [END database_input_data] + } + + void databaseInputExample() throws ExecutionException, InterruptedException { + // [START database_input] + Pipeline.Snapshot results = + firestore.pipeline().database().sort(ascending(field("population"))).execute().get(); + // [END database_input] + System.out.println(results.getResults()); + } + + // https://cloud.google.com/firestore/docs/pipeline/stages/input/documents + void documentInputSyntaxExample() throws ExecutionException, InterruptedException { + // [START document_input_syntax] + Pipeline.Snapshot results = + firestore + .pipeline() + .documents( + firestore.collection("cities").document("SF"), + firestore.collection("cities").document("NY")) + .execute() + .get(); + // [END document_input_syntax] + System.out.println(results.getResults()); + } + + void documentInputExampleData() { + // [START document_input_data] + firestore + .collection("cities") + .document("SF") + .set( + new HashMap() { + { + put("name", "San Francisco"); + put("state", "California"); + } + }); + firestore + .collection("cities") + .document("NYC") + .set( + new HashMap() { + { + put("name", "New York City"); + put("state", "New York"); + } + }); + firestore + .collection("cities") + .document("CHI") + .set( + new HashMap() { + { + put("name", "Chicago"); + put("state", "Illinois"); + } + }); + // [END document_input_data] + } + + void documentInputExample() throws ExecutionException, InterruptedException { + // [START document_input] + Pipeline.Snapshot results = + firestore + .pipeline() + .documents( + firestore.collection("cities").document("SF"), + firestore.collection("cities").document("NYC")) + .sort(ascending(field("name"))) + .execute() + .get(); + // [END document_input] + System.out.println(results.getResults()); + } + + // https://cloud.google.com/firestore/docs/pipeline/stages/transformation/union + void unionSyntaxExample() throws ExecutionException, InterruptedException { + // [START union_syntax] + Pipeline.Snapshot results = + firestore + .pipeline() + .collection("cities/SF/restaurants") + .union(firestore.pipeline().collection("cities/NYC/restaurants")) + .execute() + .get(); + // [END union_syntax] + System.out.println(results.getResults()); + } + + // https://cloud.google.com/firestore/docs/pipeline/stages/transformation/aggregate + void aggregateSyntaxExample() throws ExecutionException, InterruptedException { + // [START aggregate_syntax] + Pipeline.Snapshot cities = + firestore + .pipeline() + .collection("cities") + .aggregate(countAll().as("total"), average("population").as("averagePopulation")) + .execute() + .get(); + // [END aggregate_syntax] + System.out.println(cities.getResults()); + } + + void aggregateGroupSyntax() throws ExecutionException, InterruptedException { + // [START aggregate_group_syntax] + Pipeline.Snapshot result = + firestore + .pipeline() + .collectionGroup("cities") + .aggregate( + Aggregate.withAccumulators( + countAll().as("cities"), sum("population").as("totalPopulation")) + .withGroups(field("location.state").as("state"))) + .execute() + .get(); + // [END aggregate_group_syntax] + System.out.println(result.getResults()); + } + + void aggregateExampleData() { + // [START aggregate_data] + firestore + .collection("cities") + .document("SF") + .set( + new HashMap() { + { + put("name", "San Francisco"); + put("state", "CA"); + put("country", "USA"); + put("population", 870000); + } + }); + firestore + .collection("cities") + .document("LA") + .set( + new HashMap() { + { + put("name", "Los Angeles"); + put("state", "CA"); + put("country", "USA"); + put("population", 3970000); + } + }); + firestore + .collection("cities") + .document("NY") + .set( + new HashMap() { + { + put("name", "New York"); + put("state", "NY"); + put("country", "USA"); + put("population", 8530000); + } + }); + firestore + .collection("cities") + .document("TOR") + .set( + new HashMap() { + { + put("name", "Toronto"); + put("state", null); + put("country", "Canada"); + put("population", 2930000); + } + }); + firestore + .collection("cities") + .document("MEX") + .set( + new HashMap() { + { + put("name", "Mexico City"); + put("state", null); + put("country", "Mexico"); + put("population", 9200000); + } + }); + // [END aggregate_data] + } + + void aggregateWithoutGroupExample() throws ExecutionException, InterruptedException { + // [START aggregate_without_group] + Pipeline.Snapshot cities = + firestore + .pipeline() + .collection("cities") + .aggregate(countAll().as("total"), average("population").as("averagePopulation")) + .execute() + .get(); + // [END aggregate_without_group] + System.out.println(cities.getResults()); + } + + void aggregateGroupExample() throws ExecutionException, InterruptedException { + // [START aggregate_group_example] + Pipeline.Snapshot cities = + firestore + .pipeline() + .collection("cities") + .aggregate( + Aggregate.withAccumulators( + countAll().as("numberOfCities"), maximum("population").as("maxPopulation")) + .withGroups("country", "state")) + .execute() + .get(); + // [END aggregate_group_example] + System.out.println(cities.getResults()); + } + + void aggregateGroupComplexExample() throws ExecutionException, InterruptedException { + // [START aggregate_group_complex] + Pipeline.Snapshot cities = + firestore + .pipeline() + .collection("cities") + .aggregate( + Aggregate.withAccumulators(sum("population").as("totalPopulation")) + .withGroups(field("state").equal(null).as("stateIsNull"))) + .execute() + .get(); + // [END aggregate_group_complex] + System.out.println(cities.getResults()); + } + + // https://cloud.google.com/firestore/docs/pipeline/stages/transformation/distinct + void distinctSyntaxExample() throws ExecutionException, InterruptedException { + // [START distinct_syntax] + Pipeline.Snapshot cities1 = + firestore.pipeline().collection("cities").distinct("country").execute().get(); + + Pipeline.Snapshot cities2 = + firestore + .pipeline() + .collection("cities") + .distinct(toLower(field("state")).as("normalizedState"), field("country")) + .execute() + .get(); + // [END distinct_syntax] + System.out.println(cities1.getResults()); + System.out.println(cities2.getResults()); + } + + void distinctExampleData() { + // [START distinct_data] + firestore + .collection("cities") + .document("SF") + .set( + new HashMap() { + { + put("name", "San Francisco"); + put("state", "CA"); + put("country", "USA"); + } + }); + firestore + .collection("cities") + .document("LA") + .set( + new HashMap() { + { + put("name", "Los Angeles"); + put("state", "CA"); + put("country", "USA"); + } + }); + firestore + .collection("cities") + .document("NY") + .set( + new HashMap() { + { + put("name", "New York"); + put("state", "NY"); + put("country", "USA"); + } + }); + firestore + .collection("cities") + .document("TOR") + .set( + new HashMap() { + { + put("name", "Toronto"); + put("state", null); + put("country", "Canada"); + } + }); + firestore + .collection("cities") + .document("MEX") + .set( + new HashMap() { + { + put("name", "Mexico City"); + put("state", null); + put("country", "Mexico"); + } + }); + // [END distinct_data] + } + + void distinctExample() throws ExecutionException, InterruptedException { + // [START distinct_example] + Pipeline.Snapshot cities = + firestore.pipeline().collection("cities").distinct("country").execute().get(); + // [END distinct_example] + System.out.println(cities.getResults()); + } + + void distinctExpressionsExample() throws ExecutionException, InterruptedException { + // [START distinct_expressions] + Pipeline.Snapshot cities = + firestore + .pipeline() + .collection("cities") + .distinct(toLower(field("state")).as("normalizedState"), field("country")) + .execute() + .get(); + // [END distinct_expressions] + System.out.println(cities.getResults()); + } +} diff --git a/tools/api.txt b/tools/api.txt index ef31d240e..24339f5e9 100644 --- a/tools/api.txt +++ b/tools/api.txt @@ -3,85 +3,85 @@ package com.google.cloud.firestore { public abstract class AggregateField { ctor public AggregateField(); - method @NonNull public static com.google.cloud.firestore.AggregateField.AverageAggregateField average(@NonNull com.google.cloud.firestore.FieldPath); - method @NonNull public static com.google.cloud.firestore.AggregateField.AverageAggregateField average(@NonNull String); - method @NonNull public static com.google.cloud.firestore.AggregateField.CountAggregateField count(); + method public static com.google.cloud.firestore.AggregateField.AverageAggregateField average(com.google.cloud.firestore.FieldPath); + method public static com.google.cloud.firestore.AggregateField.AverageAggregateField average(String); + method public static com.google.cloud.firestore.AggregateField.CountAggregateField count(); method public boolean equals(Object); method public int hashCode(); - method @NonNull public static com.google.cloud.firestore.AggregateField.SumAggregateField sum(@NonNull com.google.cloud.firestore.FieldPath); - method @NonNull public static com.google.cloud.firestore.AggregateField.SumAggregateField sum(@NonNull String); + method public static com.google.cloud.firestore.AggregateField.SumAggregateField sum(com.google.cloud.firestore.FieldPath); + method public static com.google.cloud.firestore.AggregateField.SumAggregateField sum(String); } public static class AggregateField.AverageAggregateField extends com.google.cloud.firestore.AggregateField { - method @NonNull public String getOperator(); + method public String getOperator(); } public static class AggregateField.CountAggregateField extends com.google.cloud.firestore.AggregateField { - method @NonNull public String getOperator(); + method public String getOperator(); } public static class AggregateField.SumAggregateField extends com.google.cloud.firestore.AggregateField { - method @NonNull public String getOperator(); + method public String getOperator(); } public class AggregateQuery { method public boolean equals(Object); - method @NonNull public ApiFuture> explain(com.google.cloud.firestore.ExplainOptions); - method @NonNull public static com.google.cloud.firestore.AggregateQuery fromProto(com.google.cloud.firestore.Firestore, RunAggregationQueryRequest); - method @NonNull public ApiFuture get(); - method @NonNull public com.google.cloud.firestore.Query getQuery(); + method public ApiFuture> explain(com.google.cloud.firestore.ExplainOptions); + method public static com.google.cloud.firestore.AggregateQuery fromProto(com.google.cloud.firestore.Firestore, RunAggregationQueryRequest); + method public ApiFuture get(); + method public com.google.cloud.firestore.Query getQuery(); method public int hashCode(); - method @NonNull public RunAggregationQueryRequest toProto(); + method public RunAggregationQueryRequest toProto(); } public class AggregateQuerySnapshot { method public boolean equals(Object); - method @Nullable public Object get(@NonNull com.google.cloud.firestore.AggregateField); - method @Nullable public Double get(@NonNull com.google.cloud.firestore.AggregateField.AverageAggregateField); - method public long get(@NonNull com.google.cloud.firestore.AggregateField.CountAggregateField); + method @Nullable public Object get(com.google.cloud.firestore.AggregateField); + method @Nullable public Double get(com.google.cloud.firestore.AggregateField.AverageAggregateField); + method public long get(com.google.cloud.firestore.AggregateField.CountAggregateField); method public long getCount(); - method @Nullable public Double getDouble(@NonNull com.google.cloud.firestore.AggregateField); - method @Nullable public Long getLong(@NonNull com.google.cloud.firestore.AggregateField); - method @NonNull public com.google.cloud.firestore.AggregateQuery getQuery(); - method @NonNull public Timestamp getReadTime(); + method @Nullable public Double getDouble(com.google.cloud.firestore.AggregateField); + method @Nullable public Long getLong(com.google.cloud.firestore.AggregateField); + method public com.google.cloud.firestore.AggregateQuery getQuery(); + method public Timestamp getReadTime(); method public int hashCode(); } public abstract class BasePath> { ctor public BasePath(); - method public int compareTo(@NonNull B); + method public int compareTo(B); } public final class Blob { method public boolean equals(Object); - method @NonNull public static com.google.cloud.firestore.Blob fromByteString(@NonNull ByteString); - method @NonNull public static com.google.cloud.firestore.Blob fromBytes(@NonNull byte[]); + method public static com.google.cloud.firestore.Blob fromByteString(ByteString); + method public static com.google.cloud.firestore.Blob fromBytes(byte[]); method public int hashCode(); - method @NonNull public ByteString toByteString(); - method @NonNull public byte[] toBytes(); + method public ByteString toByteString(); + method public byte[] toBytes(); } public final class BulkWriter { method public void addWriteErrorListener(com.google.cloud.firestore.BulkWriter.WriteErrorCallback); - method public void addWriteErrorListener(@NonNull Executor, com.google.cloud.firestore.BulkWriter.WriteErrorCallback); + method public void addWriteErrorListener(Executor, com.google.cloud.firestore.BulkWriter.WriteErrorCallback); method public void addWriteResultListener(com.google.cloud.firestore.BulkWriter.WriteResultCallback); - method public void addWriteResultListener(@NonNull Executor, com.google.cloud.firestore.BulkWriter.WriteResultCallback); + method public void addWriteResultListener(Executor, com.google.cloud.firestore.BulkWriter.WriteResultCallback); method public void close(); - method @NonNull public ApiFuture create(@NonNull com.google.cloud.firestore.DocumentReference, @NonNull Map); - method @NonNull public ApiFuture create(@NonNull com.google.cloud.firestore.DocumentReference, @NonNull Object); - method @NonNull public ApiFuture delete(@NonNull com.google.cloud.firestore.DocumentReference); - method @NonNull public ApiFuture delete(@NonNull com.google.cloud.firestore.DocumentReference, @NonNull com.google.cloud.firestore.Precondition); - method @NonNull public ApiFuture flush(); - method @NonNull public ApiFuture set(@NonNull com.google.cloud.firestore.DocumentReference, @NonNull Map); - method @NonNull public ApiFuture set(@NonNull com.google.cloud.firestore.DocumentReference, @NonNull Map, @NonNull com.google.cloud.firestore.SetOptions); - method @NonNull public ApiFuture set(@NonNull com.google.cloud.firestore.DocumentReference, @NonNull Object); - method @NonNull public ApiFuture set(@NonNull com.google.cloud.firestore.DocumentReference, @NonNull Object, @NonNull com.google.cloud.firestore.SetOptions); - method @NonNull public ApiFuture update(@NonNull com.google.cloud.firestore.DocumentReference, @NonNull com.google.cloud.firestore.FieldPath, @Nullable Object, Object...); - method @NonNull public ApiFuture update(@NonNull com.google.cloud.firestore.DocumentReference, @NonNull com.google.cloud.firestore.Precondition, @NonNull com.google.cloud.firestore.FieldPath, @Nullable Object, Object...); - method @NonNull public ApiFuture update(@NonNull com.google.cloud.firestore.DocumentReference, @NonNull com.google.cloud.firestore.Precondition, @NonNull String, @Nullable Object, Object...); - method @NonNull public ApiFuture update(@NonNull com.google.cloud.firestore.DocumentReference, @NonNull Map); - method @NonNull public ApiFuture update(@NonNull com.google.cloud.firestore.DocumentReference, @NonNull Map, @NonNull com.google.cloud.firestore.Precondition); - method @NonNull public ApiFuture update(@NonNull com.google.cloud.firestore.DocumentReference, @NonNull String, @Nullable Object, Object...); + method public ApiFuture create(com.google.cloud.firestore.DocumentReference, Map); + method public ApiFuture create(com.google.cloud.firestore.DocumentReference, Object); + method public ApiFuture delete(com.google.cloud.firestore.DocumentReference); + method public ApiFuture delete(com.google.cloud.firestore.DocumentReference, com.google.cloud.firestore.Precondition); + method public ApiFuture flush(); + method public ApiFuture set(com.google.cloud.firestore.DocumentReference, Map); + method public ApiFuture set(com.google.cloud.firestore.DocumentReference, Map, com.google.cloud.firestore.SetOptions); + method public ApiFuture set(com.google.cloud.firestore.DocumentReference, Object); + method public ApiFuture set(com.google.cloud.firestore.DocumentReference, Object, com.google.cloud.firestore.SetOptions); + method public ApiFuture update(com.google.cloud.firestore.DocumentReference, com.google.cloud.firestore.FieldPath, @Nullable Object, Object...); + method public ApiFuture update(com.google.cloud.firestore.DocumentReference, com.google.cloud.firestore.Precondition, com.google.cloud.firestore.FieldPath, @Nullable Object, Object...); + method public ApiFuture update(com.google.cloud.firestore.DocumentReference, com.google.cloud.firestore.Precondition, String, @Nullable Object, Object...); + method public ApiFuture update(com.google.cloud.firestore.DocumentReference, Map); + method public ApiFuture update(com.google.cloud.firestore.DocumentReference, Map, com.google.cloud.firestore.Precondition); + method public ApiFuture update(com.google.cloud.firestore.DocumentReference, String, @Nullable Object, Object...); field public static final int MAX_BATCH_SIZE = 20; // 0x14 field public static final int MAX_RETRY_ATTEMPTS = 10; // 0xa field public static final int RETRY_MAX_BATCH_SIZE = 10; // 0xa @@ -116,7 +116,7 @@ package com.google.cloud.firestore { public abstract static class BulkWriterOptions.Builder { ctor public BulkWriterOptions.Builder(); method public abstract com.google.cloud.firestore.BulkWriterOptions autoBuild(); - method @NonNull public com.google.cloud.firestore.BulkWriterOptions build(); + method public com.google.cloud.firestore.BulkWriterOptions build(); method public abstract com.google.cloud.firestore.BulkWriterOptions.Builder setExecutor(@Nullable ScheduledExecutorService); method public com.google.cloud.firestore.BulkWriterOptions.Builder setInitialOpsPerSecond(int); method public com.google.cloud.firestore.BulkWriterOptions.Builder setMaxOpsPerSecond(int); @@ -129,22 +129,22 @@ package com.google.cloud.firestore { } public class CollectionReference extends com.google.cloud.firestore.Query { - method @NonNull public ApiFuture add(@NonNull Map); + method public ApiFuture add(Map); method public ApiFuture add(Object); - method @NonNull public com.google.cloud.firestore.DocumentReference document(); - method @NonNull public com.google.cloud.firestore.DocumentReference document(@NonNull String); - method @NonNull public String getId(); + method public com.google.cloud.firestore.DocumentReference document(); + method public com.google.cloud.firestore.DocumentReference document(String); + method public String getId(); method @Nullable public com.google.cloud.firestore.DocumentReference getParent(); - method @NonNull public String getPath(); - method @NonNull public Iterable listDocuments(); + method public String getPath(); + method public Iterable listDocuments(); } public class DocumentChange { method public boolean equals(Object); - method @NonNull public com.google.cloud.firestore.QueryDocumentSnapshot getDocument(); + method public com.google.cloud.firestore.QueryDocumentSnapshot getDocument(); method public int getNewIndex(); method public int getOldIndex(); - method @NonNull public com.google.cloud.firestore.DocumentChange.Type getType(); + method public com.google.cloud.firestore.DocumentChange.Type getType(); method public int hashCode(); } @@ -155,60 +155,61 @@ package com.google.cloud.firestore { } public class DocumentReference { - method @NonNull public com.google.cloud.firestore.ListenerRegistration addSnapshotListener(@NonNull com.google.cloud.firestore.EventListener); - method @NonNull public com.google.cloud.firestore.ListenerRegistration addSnapshotListener(@NonNull Executor, @NonNull com.google.cloud.firestore.EventListener); - method @NonNull public com.google.cloud.firestore.CollectionReference collection(@NonNull String); - method @NonNull public ApiFuture create(@NonNull Map); - method @NonNull public ApiFuture create(@NonNull Object); - method @NonNull public ApiFuture delete(); - method @NonNull public ApiFuture delete(@NonNull com.google.cloud.firestore.Precondition); + method public com.google.cloud.firestore.ListenerRegistration addSnapshotListener(com.google.cloud.firestore.EventListener); + method public com.google.cloud.firestore.ListenerRegistration addSnapshotListener(Executor, com.google.cloud.firestore.EventListener); + method public com.google.cloud.firestore.CollectionReference collection(String); + method public ApiFuture create(Map); + method public ApiFuture create(Object); + method public ApiFuture delete(); + method public ApiFuture delete(com.google.cloud.firestore.Precondition); method public boolean equals(Object); - method @NonNull public ApiFuture get(); - method @NonNull public ApiFuture get(com.google.cloud.firestore.FieldMask); - method @NonNull public com.google.cloud.firestore.Firestore getFirestore(); - method @NonNull public String getId(); - method @NonNull public com.google.cloud.firestore.CollectionReference getParent(); - method @NonNull public String getPath(); + method public ApiFuture get(); + method public ApiFuture get(com.google.cloud.firestore.FieldMask); + method public com.google.cloud.firestore.Firestore getFirestore(); + method public String getId(); + method public com.google.cloud.firestore.CollectionReference getParent(); + method public String getPath(); method public int hashCode(); - method @NonNull public Iterable listCollections(); - method @NonNull public ApiFuture set(@NonNull Map); - method @NonNull public ApiFuture set(@NonNull Map, @NonNull com.google.cloud.firestore.SetOptions); - method @NonNull public ApiFuture set(@NonNull Object); - method @NonNull public ApiFuture set(@NonNull Object, @NonNull com.google.cloud.firestore.SetOptions); + method public Iterable listCollections(); + method public ApiFuture set(Map); + method public ApiFuture set(Map, com.google.cloud.firestore.SetOptions); + method public ApiFuture set(Object); + method public ApiFuture set(Object, com.google.cloud.firestore.SetOptions); method public String toString(); - method @NonNull public ApiFuture update(@NonNull com.google.cloud.firestore.FieldPath, @Nullable Object, Object...); - method @NonNull public ApiFuture update(@NonNull com.google.cloud.firestore.Precondition, @NonNull com.google.cloud.firestore.FieldPath, @Nullable Object, Object...); - method @NonNull public ApiFuture update(@NonNull com.google.cloud.firestore.Precondition, @NonNull String, @Nullable Object, Object...); - method @NonNull public ApiFuture update(@NonNull Map); - method @NonNull public ApiFuture update(@NonNull Map, com.google.cloud.firestore.Precondition); - method @NonNull public ApiFuture update(@NonNull String, @Nullable Object, Object...); + method public ApiFuture update(com.google.cloud.firestore.FieldPath, @Nullable Object, Object...); + method public ApiFuture update(com.google.cloud.firestore.Precondition, com.google.cloud.firestore.FieldPath, @Nullable Object, Object...); + method public ApiFuture update(com.google.cloud.firestore.Precondition, String, @Nullable Object, Object...); + method public ApiFuture update(Map); + method public ApiFuture update(Map, com.google.cloud.firestore.Precondition); + method public ApiFuture update(String, @Nullable Object, Object...); } public class DocumentSnapshot { - method public boolean contains(@NonNull com.google.cloud.firestore.FieldPath); - method public boolean contains(@NonNull String); + method public boolean contains(com.google.cloud.firestore.FieldPath); + method public boolean contains(String); method public boolean equals(Object); method public boolean exists(); - method @Nullable public Object get(@NonNull com.google.cloud.firestore.FieldPath); - method @Nullable public T get(@NonNull com.google.cloud.firestore.FieldPath, Class); - method @Nullable public Object get(@NonNull String); - method @Nullable public T get(@NonNull String, @NonNull Class); - method @Nullable public com.google.cloud.firestore.Blob getBlob(@NonNull String); - method @Nullable public Boolean getBoolean(@NonNull String); + method @Nullable public Object get(com.google.cloud.firestore.FieldPath); + method @Nullable public T get(com.google.cloud.firestore.FieldPath, Class); + method @Nullable public Object get(String); + method @Nullable public T get(String, Class); + method @Nullable public com.google.cloud.firestore.Blob getBlob(String); + method @Nullable public Boolean getBoolean(String); method @Nullable public Timestamp getCreateTime(); method @Nullable public Map getData(); - method @Nullable public Date getDate(@NonNull String); - method @Nullable public Double getDouble(@NonNull String); - method @Nullable public com.google.cloud.firestore.GeoPoint getGeoPoint(@NonNull String); - method @NonNull public String getId(); - method @Nullable public Long getLong(@NonNull String); + method @Nullable public Date getDate(String); + method @Nullable public Double getDouble(String); + method @Nullable public com.google.cloud.firestore.GeoPoint getGeoPoint(String); + method public String getId(); + method @Nullable public Long getLong(String); method @Nullable public Timestamp getReadTime(); - method @NonNull public com.google.cloud.firestore.DocumentReference getReference(); - method @Nullable public String getString(@NonNull String); - method @Nullable public Timestamp getTimestamp(@NonNull String); + method public com.google.cloud.firestore.DocumentReference getReference(); + method @Nullable public String getString(String); + method @Nullable public Timestamp getTimestamp(String); method @Nullable public Timestamp getUpdateTime(); + method @Nullable public com.google.cloud.firestore.VectorValue getVectorValue(String); method public int hashCode(); - method @Nullable public T toObject(@NonNull Class); + method @Nullable public T toObject(Class); method public String toString(); } @@ -217,15 +218,15 @@ package com.google.cloud.firestore { } public final class ExecutionStats { - method @NonNull public Map getDebugStats(); - method @NonNull public Duration getExecutionDuration(); + method public Map getDebugStats(); + method public Duration getExecutionDuration(); method public long getReadOperations(); method public long getResultsReturned(); } public final class ExplainMetrics { method @Nullable public com.google.cloud.firestore.ExecutionStats getExecutionStats(); - method @NonNull public com.google.cloud.firestore.PlanSummary getPlanSummary(); + method public com.google.cloud.firestore.PlanSummary getPlanSummary(); } public abstract class ExplainOptions { @@ -243,82 +244,90 @@ package com.google.cloud.firestore { } public final class ExplainResults { - method @NonNull public com.google.cloud.firestore.ExplainMetrics getMetrics(); + method public com.google.cloud.firestore.ExplainMetrics getMetrics(); method @Nullable public T getSnapshot(); } + public final class ExplainStats { + method public Any getRawData(); + method public String getText(); + } + public final class FieldMask { - method @NonNull public static com.google.cloud.firestore.FieldMask of(com.google.cloud.firestore.FieldPath...); - method @NonNull public static com.google.cloud.firestore.FieldMask of(String...); + method public static com.google.cloud.firestore.FieldMask of(com.google.cloud.firestore.FieldPath...); + method public static com.google.cloud.firestore.FieldMask of(String...); } public abstract class FieldPath extends com.google.cloud.firestore.BasePath { ctor public FieldPath(); method public static com.google.cloud.firestore.FieldPath documentId(); + method public static com.google.cloud.firestore.FieldPath fromDotSeparatedString(String); method public static com.google.cloud.firestore.FieldPath fromServerFormat(String); - method public static com.google.cloud.firestore.FieldPath of(@NonNull String...); + method public static com.google.cloud.firestore.FieldPath of(String...); method public String toString(); } public abstract class FieldValue { - method @NonNull public static com.google.cloud.firestore.FieldValue arrayRemove(@NonNull Object...); - method @NonNull public static com.google.cloud.firestore.FieldValue arrayUnion(@NonNull Object...); - method @NonNull public static com.google.cloud.firestore.FieldValue delete(); + method public static com.google.cloud.firestore.FieldValue arrayRemove(Object...); + method public static com.google.cloud.firestore.FieldValue arrayUnion(Object...); + method public static com.google.cloud.firestore.FieldValue delete(); method public boolean equals(Object); method public int hashCode(); - method @NonNull public static com.google.cloud.firestore.FieldValue increment(double); - method @NonNull public static com.google.cloud.firestore.FieldValue increment(long); - method @NonNull public static com.google.cloud.firestore.FieldValue serverTimestamp(); + method public static com.google.cloud.firestore.FieldValue increment(double); + method public static com.google.cloud.firestore.FieldValue increment(long); + method public static com.google.cloud.firestore.FieldValue serverTimestamp(); + method public static com.google.cloud.firestore.VectorValue vector(double[]); } public class Filter { ctor public Filter(); - method @NonNull public static com.google.cloud.firestore.Filter and(com.google.cloud.firestore.Filter...); - method @NonNull public static com.google.cloud.firestore.Filter arrayContains(@NonNull com.google.cloud.firestore.FieldPath, @Nullable Object); - method @NonNull public static com.google.cloud.firestore.Filter arrayContains(@NonNull String, @Nullable Object); - method @NonNull public static com.google.cloud.firestore.Filter arrayContainsAny(@NonNull com.google.cloud.firestore.FieldPath, @Nullable Object); - method @NonNull public static com.google.cloud.firestore.Filter arrayContainsAny(@NonNull String, @Nullable Object); - method @NonNull public static com.google.cloud.firestore.Filter equalTo(@NonNull com.google.cloud.firestore.FieldPath, @Nullable Object); - method @NonNull public static com.google.cloud.firestore.Filter equalTo(@NonNull String, @Nullable Object); - method @NonNull public static com.google.cloud.firestore.Filter greaterThan(@NonNull com.google.cloud.firestore.FieldPath, @Nullable Object); - method @NonNull public static com.google.cloud.firestore.Filter greaterThan(@NonNull String, @Nullable Object); - method @NonNull public static com.google.cloud.firestore.Filter greaterThanOrEqualTo(@NonNull com.google.cloud.firestore.FieldPath, @Nullable Object); - method @NonNull public static com.google.cloud.firestore.Filter greaterThanOrEqualTo(@NonNull String, @Nullable Object); - method @NonNull public static com.google.cloud.firestore.Filter inArray(@NonNull com.google.cloud.firestore.FieldPath, @Nullable Object); - method @NonNull public static com.google.cloud.firestore.Filter inArray(@NonNull String, @Nullable Object); - method @NonNull public static com.google.cloud.firestore.Filter lessThan(@NonNull com.google.cloud.firestore.FieldPath, @Nullable Object); - method @NonNull public static com.google.cloud.firestore.Filter lessThan(@NonNull String, @Nullable Object); - method @NonNull public static com.google.cloud.firestore.Filter lessThanOrEqualTo(@NonNull com.google.cloud.firestore.FieldPath, @Nullable Object); - method @NonNull public static com.google.cloud.firestore.Filter lessThanOrEqualTo(@NonNull String, @Nullable Object); - method @NonNull public static com.google.cloud.firestore.Filter notEqualTo(@NonNull com.google.cloud.firestore.FieldPath, @Nullable Object); - method @NonNull public static com.google.cloud.firestore.Filter notEqualTo(@NonNull String, @Nullable Object); - method @NonNull public static com.google.cloud.firestore.Filter notInArray(@NonNull com.google.cloud.firestore.FieldPath, @Nullable Object); - method @NonNull public static com.google.cloud.firestore.Filter notInArray(@NonNull String, @Nullable Object); - method @NonNull public static com.google.cloud.firestore.Filter or(com.google.cloud.firestore.Filter...); + method public static com.google.cloud.firestore.Filter and(com.google.cloud.firestore.Filter...); + method public static com.google.cloud.firestore.Filter arrayContains(com.google.cloud.firestore.FieldPath, @Nullable Object); + method public static com.google.cloud.firestore.Filter arrayContains(String, @Nullable Object); + method public static com.google.cloud.firestore.Filter arrayContainsAny(com.google.cloud.firestore.FieldPath, @Nullable Object); + method public static com.google.cloud.firestore.Filter arrayContainsAny(String, @Nullable Object); + method public static com.google.cloud.firestore.Filter equalTo(com.google.cloud.firestore.FieldPath, @Nullable Object); + method public static com.google.cloud.firestore.Filter equalTo(String, @Nullable Object); + method public static com.google.cloud.firestore.Filter greaterThan(com.google.cloud.firestore.FieldPath, @Nullable Object); + method public static com.google.cloud.firestore.Filter greaterThan(String, @Nullable Object); + method public static com.google.cloud.firestore.Filter greaterThanOrEqualTo(com.google.cloud.firestore.FieldPath, @Nullable Object); + method public static com.google.cloud.firestore.Filter greaterThanOrEqualTo(String, @Nullable Object); + method public static com.google.cloud.firestore.Filter inArray(com.google.cloud.firestore.FieldPath, @Nullable Object); + method public static com.google.cloud.firestore.Filter inArray(String, @Nullable Object); + method public static com.google.cloud.firestore.Filter lessThan(com.google.cloud.firestore.FieldPath, @Nullable Object); + method public static com.google.cloud.firestore.Filter lessThan(String, @Nullable Object); + method public static com.google.cloud.firestore.Filter lessThanOrEqualTo(com.google.cloud.firestore.FieldPath, @Nullable Object); + method public static com.google.cloud.firestore.Filter lessThanOrEqualTo(String, @Nullable Object); + method public static com.google.cloud.firestore.Filter notEqualTo(com.google.cloud.firestore.FieldPath, @Nullable Object); + method public static com.google.cloud.firestore.Filter notEqualTo(String, @Nullable Object); + method public static com.google.cloud.firestore.Filter notInArray(com.google.cloud.firestore.FieldPath, @Nullable Object); + method public static com.google.cloud.firestore.Filter notInArray(String, @Nullable Object); + method public static com.google.cloud.firestore.Filter or(com.google.cloud.firestore.Filter...); } public interface Firestore { - method @NonNull public com.google.cloud.firestore.WriteBatch batch(); - method @NonNull public com.google.cloud.firestore.BulkWriter bulkWriter(); - method @NonNull public com.google.cloud.firestore.BulkWriter bulkWriter(com.google.cloud.firestore.BulkWriterOptions); - method @NonNull public com.google.cloud.firestore.FirestoreBundle.Builder bundleBuilder(); - method @NonNull public com.google.cloud.firestore.FirestoreBundle.Builder bundleBuilder(@NonNull String); + method public com.google.cloud.firestore.WriteBatch batch(); + method public com.google.cloud.firestore.BulkWriter bulkWriter(); + method public com.google.cloud.firestore.BulkWriter bulkWriter(com.google.cloud.firestore.BulkWriterOptions); + method public com.google.cloud.firestore.FirestoreBundle.Builder bundleBuilder(); + method public com.google.cloud.firestore.FirestoreBundle.Builder bundleBuilder(String); method public void close(); - method @NonNull public com.google.cloud.firestore.CollectionReference collection(@NonNull String); - method public com.google.cloud.firestore.CollectionGroup collectionGroup(@NonNull String); - method @NonNull public com.google.cloud.firestore.DocumentReference document(@NonNull String); - method @NonNull public ApiFuture> getAll(@NonNull com.google.cloud.firestore.DocumentReference...); - method @NonNull public ApiFuture> getAll(@NonNull com.google.cloud.firestore.DocumentReference[], @Nullable com.google.cloud.firestore.FieldMask); - method public void getAll(@NonNull com.google.cloud.firestore.DocumentReference[], @Nullable com.google.cloud.firestore.FieldMask, ApiStreamObserver); - method @NonNull public Iterable listCollections(); - method @NonNull public ApiFuture recursiveDelete(com.google.cloud.firestore.CollectionReference); - method @NonNull public ApiFuture recursiveDelete(com.google.cloud.firestore.CollectionReference, com.google.cloud.firestore.BulkWriter); - method @NonNull public ApiFuture recursiveDelete(com.google.cloud.firestore.DocumentReference); - method @NonNull public ApiFuture recursiveDelete(com.google.cloud.firestore.DocumentReference, com.google.cloud.firestore.BulkWriter); - method @NonNull public ApiFuture runAsyncTransaction(@NonNull com.google.cloud.firestore.Transaction.AsyncFunction); - method @NonNull public ApiFuture runAsyncTransaction(@NonNull com.google.cloud.firestore.Transaction.AsyncFunction, @NonNull com.google.cloud.firestore.TransactionOptions); - method @NonNull public ApiFuture runTransaction(@NonNull com.google.cloud.firestore.Transaction.Function); - method @NonNull public ApiFuture runTransaction(@NonNull com.google.cloud.firestore.Transaction.Function, @NonNull com.google.cloud.firestore.TransactionOptions); + method public com.google.cloud.firestore.CollectionReference collection(String); + method public com.google.cloud.firestore.CollectionGroup collectionGroup(String); + method public com.google.cloud.firestore.DocumentReference document(String); + method public ApiFuture> getAll(com.google.cloud.firestore.DocumentReference...); + method public ApiFuture> getAll(com.google.cloud.firestore.DocumentReference[], @Nullable com.google.cloud.firestore.FieldMask); + method public void getAll(com.google.cloud.firestore.DocumentReference[], @Nullable com.google.cloud.firestore.FieldMask, ApiStreamObserver); + method public Iterable listCollections(); + method public com.google.cloud.firestore.PipelineSource pipeline(); + method public ApiFuture recursiveDelete(com.google.cloud.firestore.CollectionReference); + method public ApiFuture recursiveDelete(com.google.cloud.firestore.CollectionReference, com.google.cloud.firestore.BulkWriter); + method public ApiFuture recursiveDelete(com.google.cloud.firestore.DocumentReference); + method public ApiFuture recursiveDelete(com.google.cloud.firestore.DocumentReference, com.google.cloud.firestore.BulkWriter); + method public ApiFuture runAsyncTransaction(com.google.cloud.firestore.Transaction.AsyncFunction); + method public ApiFuture runAsyncTransaction(com.google.cloud.firestore.Transaction.AsyncFunction, com.google.cloud.firestore.TransactionOptions); + method public ApiFuture runTransaction(com.google.cloud.firestore.Transaction.Function); + method public ApiFuture runTransaction(com.google.cloud.firestore.Transaction.Function, com.google.cloud.firestore.TransactionOptions); method public void shutdown(); method public void shutdownNow(); } @@ -347,42 +356,57 @@ package com.google.cloud.firestore { public interface FirestoreFactory { } + public class FirestoreOpenTelemetryOptions { + method public OpenTelemetry getOpenTelemetry(); + method public boolean isTracingEnabled(); + method public static com.google.cloud.firestore.FirestoreOpenTelemetryOptions.Builder newBuilder(); + method public com.google.cloud.firestore.FirestoreOpenTelemetryOptions.Builder toBuilder(); + } + + public static class FirestoreOpenTelemetryOptions.Builder { + method public com.google.cloud.firestore.FirestoreOpenTelemetryOptions build(); + method public com.google.cloud.firestore.FirestoreOpenTelemetryOptions.Builder setOpenTelemetry(OpenTelemetry); + method public com.google.cloud.firestore.FirestoreOpenTelemetryOptions.Builder setTracingEnabled(boolean); + } + public final class FirestoreOptions { ctor protected FirestoreOptions(com.google.cloud.firestore.FirestoreOptions.Builder); method public boolean equals(Object); method public CredentialsProvider getCredentialsProvider(); method public String getDatabaseId(); - method @NonNull public static GoogleCredentialsProvider.Builder getDefaultCredentialsProviderBuilder(); + method public static GoogleCredentialsProvider.Builder getDefaultCredentialsProviderBuilder(); method protected String getDefaultHost(); method public static com.google.cloud.firestore.FirestoreOptions getDefaultInstance(); - method @NonNull public static InstantiatingGrpcChannelProvider.Builder getDefaultTransportChannelProviderBuilder(); - method @NonNull public static GrpcTransportOptions.Builder getDefaultTransportOptionsBuilder(); + method public static InstantiatingGrpcChannelProvider.Builder getDefaultTransportChannelProviderBuilder(); + method public static GrpcTransportOptions.Builder getDefaultTransportOptionsBuilder(); method public String getEmulatorHost(); + method public com.google.cloud.firestore.FirestoreOpenTelemetryOptions getOpenTelemetryOptions(); method protected Set getScopes(); method public TransportChannelProvider getTransportChannelProvider(); method public int hashCode(); - method @NonNull public static com.google.cloud.firestore.FirestoreOptions.Builder newBuilder(); + method public static com.google.cloud.firestore.FirestoreOptions.Builder newBuilder(); method protected boolean projectIdRequired(); - method @NonNull public com.google.cloud.firestore.FirestoreOptions.Builder toBuilder(); + method public com.google.cloud.firestore.FirestoreOptions.Builder toBuilder(); } public static class FirestoreOptions.Builder { - method @NonNull public com.google.cloud.firestore.FirestoreOptions build(); - method @NonNull public com.google.cloud.firestore.FirestoreOptions.Builder setChannelProvider(@NonNull TransportChannelProvider); - method @NonNull public com.google.cloud.firestore.FirestoreOptions.Builder setCredentialsProvider(@NonNull CredentialsProvider); - method public com.google.cloud.firestore.FirestoreOptions.Builder setDatabaseId(@NonNull String); - method public com.google.cloud.firestore.FirestoreOptions.Builder setEmulatorHost(@NonNull String); - method @NonNull public com.google.cloud.firestore.FirestoreOptions.Builder setTransportOptions(@NonNull TransportOptions); + method public com.google.cloud.firestore.FirestoreOptions build(); + method public com.google.cloud.firestore.FirestoreOptions.Builder setChannelProvider(TransportChannelProvider); + method public com.google.cloud.firestore.FirestoreOptions.Builder setCredentialsProvider(CredentialsProvider); + method public com.google.cloud.firestore.FirestoreOptions.Builder setDatabaseId(String); + method public com.google.cloud.firestore.FirestoreOptions.Builder setEmulatorHost(String); + method public com.google.cloud.firestore.FirestoreOptions.Builder setOpenTelemetryOptions(com.google.cloud.firestore.FirestoreOpenTelemetryOptions); + method public com.google.cloud.firestore.FirestoreOptions.Builder setTransportOptions(TransportOptions); } public static class FirestoreOptions.DefaultFirestoreFactory implements com.google.cloud.firestore.FirestoreFactory { ctor public FirestoreOptions.DefaultFirestoreFactory(); - method @NonNull public com.google.cloud.firestore.Firestore create(@NonNull com.google.cloud.firestore.FirestoreOptions); + method public com.google.cloud.firestore.Firestore create(com.google.cloud.firestore.FirestoreOptions); } public static class FirestoreOptions.DefaultFirestoreRpcFactory implements com.google.cloud.firestore.FirestoreRpcFactory { ctor public FirestoreOptions.DefaultFirestoreRpcFactory(); - method @NonNull public com.google.cloud.firestore.spi.v1.FirestoreRpc create(@NonNull com.google.cloud.firestore.FirestoreOptions); + method public com.google.cloud.firestore.spi.v1.FirestoreRpc create(com.google.cloud.firestore.FirestoreOptions); } public static class FirestoreOptions.EmulatorCredentials { @@ -403,7 +427,7 @@ package com.google.cloud.firestore { method public double getLatitude(); method public double getLongitude(); method public int hashCode(); - method @NonNull public String toString(); + method public String toString(); } public class Internal { @@ -420,70 +444,163 @@ package com.google.cloud.firestore { method public void remove(); } + public final class Pipeline { + method public com.google.cloud.firestore.Pipeline addFields(com.google.cloud.firestore.pipeline.expressions.Selectable...); + method public com.google.cloud.firestore.Pipeline aggregate(com.google.cloud.firestore.pipeline.expressions.AliasedAggregate...); + method public com.google.cloud.firestore.Pipeline aggregate(com.google.cloud.firestore.pipeline.stages.Aggregate); + method public com.google.cloud.firestore.Pipeline aggregate(com.google.cloud.firestore.pipeline.stages.Aggregate, com.google.cloud.firestore.pipeline.stages.AggregateOptions); + method public com.google.cloud.firestore.Pipeline distinct(com.google.cloud.firestore.pipeline.expressions.Selectable...); + method public com.google.cloud.firestore.Pipeline distinct(String...); + method public ApiFuture execute(); + method public void execute(ApiStreamObserver); + method public ApiFuture execute(com.google.cloud.firestore.pipeline.stages.PipelineExecuteOptions); + method public com.google.cloud.firestore.Pipeline findNearest(com.google.cloud.firestore.pipeline.expressions.Expression, double[], com.google.cloud.firestore.pipeline.stages.FindNearest.DistanceMeasure, com.google.cloud.firestore.pipeline.stages.FindNearestOptions); + method public com.google.cloud.firestore.Pipeline findNearest(String, double[], com.google.cloud.firestore.pipeline.stages.FindNearest.DistanceMeasure, com.google.cloud.firestore.pipeline.stages.FindNearestOptions); + method public com.google.cloud.firestore.Pipeline genericStage(String, List, com.google.cloud.firestore.pipeline.stages.RawOptions); + method public com.google.cloud.firestore.Pipeline limit(int); + method public com.google.cloud.firestore.Pipeline offset(int); + method public com.google.cloud.firestore.Pipeline removeFields(com.google.cloud.firestore.pipeline.expressions.Field...); + method public com.google.cloud.firestore.Pipeline removeFields(String...); + method public com.google.cloud.firestore.Pipeline replaceWith(com.google.cloud.firestore.pipeline.expressions.Expression); + method public com.google.cloud.firestore.Pipeline replaceWith(String); + method public com.google.cloud.firestore.Pipeline sample(com.google.cloud.firestore.pipeline.stages.Sample); + method public com.google.cloud.firestore.Pipeline sample(int); + method public com.google.cloud.firestore.Pipeline select(com.google.cloud.firestore.pipeline.expressions.Selectable...); + method public com.google.cloud.firestore.Pipeline select(String...); + method public com.google.cloud.firestore.Pipeline sort(com.google.cloud.firestore.pipeline.expressions.Ordering...); + method public com.google.firestore.v1.Value toProtoValue(); + method public com.google.cloud.firestore.Pipeline union(com.google.cloud.firestore.Pipeline); + method public com.google.cloud.firestore.Pipeline unnest(com.google.cloud.firestore.pipeline.expressions.Selectable); + method public com.google.cloud.firestore.Pipeline unnest(String, String); + method public com.google.cloud.firestore.Pipeline unnest(String, String, com.google.cloud.firestore.pipeline.stages.UnnestOptions); + method public com.google.cloud.firestore.Pipeline where(com.google.cloud.firestore.pipeline.expressions.BooleanExpression); + } + + public final class PipelineResult { + method public boolean contains(com.google.cloud.firestore.FieldPath); + method public boolean contains(String); + method public boolean equals(Object); + method public boolean exists(); + method @Nullable public Object get(com.google.cloud.firestore.FieldPath); + method @Nullable public T get(com.google.cloud.firestore.FieldPath, Class); + method @Nullable public Object get(String); + method @Nullable public T get(String, Class); + method @Nullable public com.google.cloud.firestore.Blob getBlob(String); + method @Nullable public Boolean getBoolean(String); + method @Nullable public Timestamp getCreateTime(); + method public Map getData(); + method @Nullable public Date getDate(String); + method @Nullable public Double getDouble(String); + method @Nullable public Timestamp getExecutionTime(); + method @Nullable public com.google.cloud.firestore.GeoPoint getGeoPoint(String); + method @Nullable public String getId(); + method @Nullable public Long getLong(String); + method public com.google.cloud.firestore.DocumentReference getReference(); + method @Nullable public String getString(String); + method @Nullable public Timestamp getTimestamp(String); + method @Nullable public Timestamp getUpdateTime(); + method public int hashCode(); + method @Nullable public T toObject(Class); + method public String toString(); + } + + public final class PipelineSnapshot { + method public Timestamp getExecutionTime(); + method @Nullable public com.google.cloud.firestore.ExplainStats getExplainStats(); + method public com.google.cloud.firestore.Pipeline getPipeline(); + method public List getResults(); + } + + public final class PipelineSource { + method public com.google.cloud.firestore.Pipeline collection(com.google.cloud.firestore.CollectionReference); + method public com.google.cloud.firestore.Pipeline collection(String); + method public com.google.cloud.firestore.Pipeline collection(String, com.google.cloud.firestore.pipeline.stages.CollectionOptions); + method public com.google.cloud.firestore.Pipeline collectionGroup(String); + method public com.google.cloud.firestore.Pipeline collectionGroup(String, com.google.cloud.firestore.pipeline.stages.CollectionGroupOptions); + method public com.google.cloud.firestore.Pipeline createFrom(com.google.cloud.firestore.AggregateQuery); + method public com.google.cloud.firestore.Pipeline createFrom(com.google.cloud.firestore.Query); + method public com.google.cloud.firestore.Pipeline database(); + method public com.google.cloud.firestore.Pipeline documents(com.google.cloud.firestore.DocumentReference...); + } + + public class PipelineUtils { + ctor public PipelineUtils(); + method public static Value encodeValue(boolean); + method public static Value encodeValue(com.google.cloud.firestore.pipeline.expressions.AggregateFunction); + method public static Value encodeValue(com.google.cloud.firestore.pipeline.expressions.Expression); + method public static Value encodeValue(double); + method public static Value encodeValue(long); + method public static Value encodeValue(Map); + method public static Value encodeValue(Object); + method public static Value encodeValue(String); + method public static Map fieldNamesToMap(String...); + method public static Map selectablesToMap(com.google.cloud.firestore.pipeline.expressions.Selectable...); + } + public final class PlanSummary { - method @NonNull public List> getIndexesUsed(); + method public List> getIndexesUsed(); } public final class Precondition { method public boolean equals(Object); method public int hashCode(); - method @NonNull public static com.google.cloud.firestore.Precondition updatedAt(Timestamp); + method public static com.google.cloud.firestore.Precondition updatedAt(Timestamp); field public static final com.google.cloud.firestore.Precondition NONE; } public class Query { ctor protected Query(com.google.cloud.firestore.FirestoreRpcContext, com.google.cloud.firestore.Query.QueryOptions); - method @NonNull public com.google.cloud.firestore.ListenerRegistration addSnapshotListener(@NonNull com.google.cloud.firestore.EventListener); - method @NonNull public com.google.cloud.firestore.ListenerRegistration addSnapshotListener(@NonNull Executor, @NonNull com.google.cloud.firestore.EventListener); - method @NonNull public com.google.cloud.firestore.AggregateQuery aggregate(@NonNull com.google.cloud.firestore.AggregateField, @NonNull com.google.cloud.firestore.AggregateField...); - method @NonNull public com.google.cloud.firestore.AggregateQuery count(); - method @NonNull public com.google.cloud.firestore.Query endAt(@NonNull com.google.cloud.firestore.DocumentSnapshot); - method @NonNull public com.google.cloud.firestore.Query endAt(Object...); - method @NonNull public com.google.cloud.firestore.Query endBefore(@NonNull com.google.cloud.firestore.DocumentSnapshot); - method @NonNull public com.google.cloud.firestore.Query endBefore(Object...); + method public com.google.cloud.firestore.ListenerRegistration addSnapshotListener(com.google.cloud.firestore.EventListener); + method public com.google.cloud.firestore.ListenerRegistration addSnapshotListener(Executor, com.google.cloud.firestore.EventListener); + method public com.google.cloud.firestore.AggregateQuery aggregate(com.google.cloud.firestore.AggregateField, com.google.cloud.firestore.AggregateField...); + method public com.google.cloud.firestore.AggregateQuery count(); + method public com.google.cloud.firestore.Query endAt(com.google.cloud.firestore.DocumentSnapshot); + method public com.google.cloud.firestore.Query endAt(Object...); + method public com.google.cloud.firestore.Query endBefore(com.google.cloud.firestore.DocumentSnapshot); + method public com.google.cloud.firestore.Query endBefore(Object...); method public boolean equals(Object); - method @NonNull public ApiFuture> explain(com.google.cloud.firestore.ExplainOptions); - method @NonNull public ApiFuture explainStream(@NonNull com.google.cloud.firestore.ExplainOptions, @NonNull ApiStreamObserver); + method public ApiFuture> explain(com.google.cloud.firestore.ExplainOptions); + method public ApiFuture explainStream(com.google.cloud.firestore.ExplainOptions, ApiStreamObserver); method public static com.google.cloud.firestore.Query fromProto(com.google.cloud.firestore.Firestore, RunQueryRequest); - method @NonNull public ApiFuture get(); - method @NonNull public com.google.cloud.firestore.Firestore getFirestore(); + method public ApiFuture get(); + method public com.google.cloud.firestore.Firestore getFirestore(); method public int hashCode(); - method @NonNull public com.google.cloud.firestore.Query limit(int); - method @NonNull public com.google.cloud.firestore.Query limitToLast(int); - method @NonNull public com.google.cloud.firestore.Query offset(int); - method @NonNull public com.google.cloud.firestore.Query orderBy(@NonNull com.google.cloud.firestore.FieldPath); - method @NonNull public com.google.cloud.firestore.Query orderBy(@NonNull com.google.cloud.firestore.FieldPath, @NonNull com.google.cloud.firestore.Query.Direction); - method @NonNull public com.google.cloud.firestore.Query orderBy(@NonNull String); - method @NonNull public com.google.cloud.firestore.Query orderBy(@NonNull String, @NonNull com.google.cloud.firestore.Query.Direction); - method @NonNull public com.google.cloud.firestore.Query select(com.google.cloud.firestore.FieldPath...); - method @NonNull public com.google.cloud.firestore.Query select(String...); - method @NonNull public com.google.cloud.firestore.Query startAfter(@NonNull com.google.cloud.firestore.DocumentSnapshot); + method public com.google.cloud.firestore.Query limit(int); + method public com.google.cloud.firestore.Query limitToLast(int); + method public com.google.cloud.firestore.Query offset(int); + method public com.google.cloud.firestore.Query orderBy(com.google.cloud.firestore.FieldPath); + method public com.google.cloud.firestore.Query orderBy(com.google.cloud.firestore.FieldPath, com.google.cloud.firestore.Query.Direction); + method public com.google.cloud.firestore.Query orderBy(String); + method public com.google.cloud.firestore.Query orderBy(String, com.google.cloud.firestore.Query.Direction); + method public com.google.cloud.firestore.Query select(com.google.cloud.firestore.FieldPath...); + method public com.google.cloud.firestore.Query select(String...); + method public com.google.cloud.firestore.Query startAfter(com.google.cloud.firestore.DocumentSnapshot); method public com.google.cloud.firestore.Query startAfter(Object...); - method @NonNull public com.google.cloud.firestore.Query startAt(@NonNull com.google.cloud.firestore.DocumentSnapshot); - method @NonNull public com.google.cloud.firestore.Query startAt(Object...); - method public void stream(@NonNull ApiStreamObserver); + method public com.google.cloud.firestore.Query startAt(com.google.cloud.firestore.DocumentSnapshot); + method public com.google.cloud.firestore.Query startAt(Object...); + method public void stream(ApiStreamObserver); method public RunQueryRequest toProto(); method public com.google.cloud.firestore.Query where(com.google.cloud.firestore.Filter); - method @NonNull public com.google.cloud.firestore.Query whereArrayContains(@NonNull com.google.cloud.firestore.FieldPath, @NonNull Object); - method @NonNull public com.google.cloud.firestore.Query whereArrayContains(@NonNull String, @NonNull Object); - method @NonNull public com.google.cloud.firestore.Query whereArrayContainsAny(@NonNull com.google.cloud.firestore.FieldPath, @NonNull List); - method @NonNull public com.google.cloud.firestore.Query whereArrayContainsAny(@NonNull String, @NonNull List); - method @NonNull public com.google.cloud.firestore.Query whereEqualTo(@NonNull com.google.cloud.firestore.FieldPath, @Nullable Object); - method @NonNull public com.google.cloud.firestore.Query whereEqualTo(@NonNull String, @Nullable Object); - method @NonNull public com.google.cloud.firestore.Query whereGreaterThan(@NonNull com.google.cloud.firestore.FieldPath, @NonNull Object); - method @NonNull public com.google.cloud.firestore.Query whereGreaterThan(@NonNull String, @NonNull Object); - method @NonNull public com.google.cloud.firestore.Query whereGreaterThanOrEqualTo(@NonNull com.google.cloud.firestore.FieldPath, @NonNull Object); - method @NonNull public com.google.cloud.firestore.Query whereGreaterThanOrEqualTo(@NonNull String, @NonNull Object); - method @NonNull public com.google.cloud.firestore.Query whereIn(@NonNull com.google.cloud.firestore.FieldPath, @NonNull List); - method @NonNull public com.google.cloud.firestore.Query whereIn(@NonNull String, @NonNull List); - method @NonNull public com.google.cloud.firestore.Query whereLessThan(@NonNull com.google.cloud.firestore.FieldPath, @NonNull Object); - method @NonNull public com.google.cloud.firestore.Query whereLessThan(@NonNull String, @NonNull Object); - method @NonNull public com.google.cloud.firestore.Query whereLessThanOrEqualTo(@NonNull com.google.cloud.firestore.FieldPath, @NonNull Object); - method @NonNull public com.google.cloud.firestore.Query whereLessThanOrEqualTo(@NonNull String, @NonNull Object); - method @NonNull public com.google.cloud.firestore.Query whereNotEqualTo(@NonNull com.google.cloud.firestore.FieldPath, @Nullable Object); - method @NonNull public com.google.cloud.firestore.Query whereNotEqualTo(@NonNull String, @Nullable Object); - method @NonNull public com.google.cloud.firestore.Query whereNotIn(@NonNull com.google.cloud.firestore.FieldPath, @NonNull List); - method @NonNull public com.google.cloud.firestore.Query whereNotIn(@NonNull String, @NonNull List); + method public com.google.cloud.firestore.Query whereArrayContains(com.google.cloud.firestore.FieldPath, Object); + method public com.google.cloud.firestore.Query whereArrayContains(String, Object); + method public com.google.cloud.firestore.Query whereArrayContainsAny(com.google.cloud.firestore.FieldPath, List); + method public com.google.cloud.firestore.Query whereArrayContainsAny(String, List); + method public com.google.cloud.firestore.Query whereEqualTo(com.google.cloud.firestore.FieldPath, @Nullable Object); + method public com.google.cloud.firestore.Query whereEqualTo(String, @Nullable Object); + method public com.google.cloud.firestore.Query whereGreaterThan(com.google.cloud.firestore.FieldPath, Object); + method public com.google.cloud.firestore.Query whereGreaterThan(String, Object); + method public com.google.cloud.firestore.Query whereGreaterThanOrEqualTo(com.google.cloud.firestore.FieldPath, Object); + method public com.google.cloud.firestore.Query whereGreaterThanOrEqualTo(String, Object); + method public com.google.cloud.firestore.Query whereIn(com.google.cloud.firestore.FieldPath, List); + method public com.google.cloud.firestore.Query whereIn(String, List); + method public com.google.cloud.firestore.Query whereLessThan(com.google.cloud.firestore.FieldPath, Object); + method public com.google.cloud.firestore.Query whereLessThan(String, Object); + method public com.google.cloud.firestore.Query whereLessThanOrEqualTo(com.google.cloud.firestore.FieldPath, Object); + method public com.google.cloud.firestore.Query whereLessThanOrEqualTo(String, Object); + method public com.google.cloud.firestore.Query whereNotEqualTo(com.google.cloud.firestore.FieldPath, @Nullable Object); + method public com.google.cloud.firestore.Query whereNotEqualTo(String, @Nullable Object); + method public com.google.cloud.firestore.Query whereNotIn(com.google.cloud.firestore.FieldPath, List); + method public com.google.cloud.firestore.Query whereNotIn(String, List); } public enum Query.Direction { @@ -506,15 +623,15 @@ package com.google.cloud.firestore { public abstract class QuerySnapshot { ctor protected QuerySnapshot(com.google.cloud.firestore.Query, Timestamp); method public abstract boolean equals(Object); - method @NonNull public abstract List getDocumentChanges(); - method @NonNull public abstract List getDocuments(); - method @NonNull public com.google.cloud.firestore.Query getQuery(); - method @NonNull public Timestamp getReadTime(); + method public abstract List getDocumentChanges(); + method public abstract List getDocuments(); + method public com.google.cloud.firestore.Query getQuery(); + method public Timestamp getReadTime(); method public abstract int hashCode(); method public boolean isEmpty(); - method @NonNull public Iterator iterator(); + method public Iterator iterator(); method public abstract int size(); - method @NonNull public List toObjects(@NonNull Class); + method public List toObjects(Class); method public static com.google.cloud.firestore.QuerySnapshot withChanges(com.google.cloud.firestore.Query, Timestamp, com.google.cloud.firestore.DocumentSet, List); method public static com.google.cloud.firestore.QuerySnapshot withDocuments(com.google.cloud.firestore.Query, Timestamp, List); } @@ -523,26 +640,29 @@ package com.google.cloud.firestore { method public ApiFuture run(); field public static final int MAX_PENDING_OPS = 5000; // 0x1388 field public static final int MIN_PENDING_OPS = 1000; // 0x3e8 - field public static final String REFERENCE_NAME_MIN_ID; + field public static final String REFERENCE_NAME_MIN_ID = "__id-9223372036854775808__"; } public final class SetOptions { method public boolean equals(Object); method public int hashCode(); - method @NonNull public static com.google.cloud.firestore.SetOptions merge(); - method @NonNull public static com.google.cloud.firestore.SetOptions mergeFieldPaths(List); - method @NonNull public static com.google.cloud.firestore.SetOptions mergeFields(List); - method @NonNull public static com.google.cloud.firestore.SetOptions mergeFields(String...); + method public static com.google.cloud.firestore.SetOptions merge(); + method public static com.google.cloud.firestore.SetOptions mergeFieldPaths(List); + method public static com.google.cloud.firestore.SetOptions mergeFields(List); + method public static com.google.cloud.firestore.SetOptions mergeFields(String...); } public abstract class Transaction extends com.google.cloud.firestore.UpdateBuilder { ctor protected Transaction(com.google.cloud.firestore.FirestoreImpl); - method @NonNull public abstract ApiFuture get(@NonNull com.google.cloud.firestore.AggregateQuery); - method @NonNull public abstract ApiFuture get(@NonNull com.google.cloud.firestore.DocumentReference); - method @NonNull public abstract ApiFuture get(@NonNull com.google.cloud.firestore.Query); - method @NonNull public abstract ApiFuture> getAll(@NonNull com.google.cloud.firestore.DocumentReference...); - method @NonNull public abstract ApiFuture> getAll(@NonNull com.google.cloud.firestore.DocumentReference[], @Nullable com.google.cloud.firestore.FieldMask); + method public abstract ApiFuture execute(com.google.cloud.firestore.Pipeline); + method public abstract ApiFuture execute(com.google.cloud.firestore.Pipeline, com.google.cloud.firestore.pipeline.stages.PipelineExecuteOptions); + method public abstract ApiFuture get(com.google.cloud.firestore.AggregateQuery); + method public abstract ApiFuture get(com.google.cloud.firestore.DocumentReference); + method public abstract ApiFuture get(com.google.cloud.firestore.Query); + method public abstract ApiFuture> getAll(com.google.cloud.firestore.DocumentReference...); + method public abstract ApiFuture> getAll(com.google.cloud.firestore.DocumentReference[], @Nullable com.google.cloud.firestore.FieldMask); method public abstract boolean hasTransactionId(); + field protected com.google.cloud.firestore.telemetry.TraceUtil.Context transactionTraceContext; } public static interface Transaction.AsyncFunction { @@ -554,36 +674,36 @@ package com.google.cloud.firestore { } public final class TransactionOptions { - method @NonNull public static com.google.cloud.firestore.TransactionOptions create(); - method @Deprecated @NonNull public static com.google.cloud.firestore.TransactionOptions create(@Nullable Executor); - method @Deprecated @NonNull public static com.google.cloud.firestore.TransactionOptions create(@Nullable Executor, int); - method @Deprecated @NonNull public static com.google.cloud.firestore.TransactionOptions create(int); - method @NonNull public static com.google.cloud.firestore.TransactionOptions.ReadOnlyOptionsBuilder createReadOnlyOptionsBuilder(); - method @NonNull public static com.google.cloud.firestore.TransactionOptions.ReadWriteOptionsBuilder createReadWriteOptionsBuilder(); + method public static com.google.cloud.firestore.TransactionOptions create(); + method @Deprecated public static com.google.cloud.firestore.TransactionOptions create(@Nullable Executor); + method @Deprecated public static com.google.cloud.firestore.TransactionOptions create(@Nullable Executor, int); + method @Deprecated public static com.google.cloud.firestore.TransactionOptions create(int); + method public static com.google.cloud.firestore.TransactionOptions.ReadOnlyOptionsBuilder createReadOnlyOptionsBuilder(); + method public static com.google.cloud.firestore.TransactionOptions.ReadWriteOptionsBuilder createReadWriteOptionsBuilder(); method @Nullable public Executor getExecutor(); method public int getNumberOfAttempts(); method @Nullable public Timestamp getReadTime(); - method @NonNull public com.google.cloud.firestore.TransactionOptions.TransactionOptionsType getType(); + method public com.google.cloud.firestore.TransactionOptions.TransactionOptionsType getType(); } public abstract static class TransactionOptions.Builder> { ctor protected TransactionOptions.Builder(@Nullable Executor); - method @NonNull public abstract com.google.cloud.firestore.TransactionOptions build(); + method public abstract com.google.cloud.firestore.TransactionOptions build(); method @Nullable public Executor getExecutor(); - method @NonNull public B setExecutor(@Nullable Executor); + method public B setExecutor(@Nullable Executor); field @Nullable protected Executor executor; } public static final class TransactionOptions.ReadOnlyOptionsBuilder extends com.google.cloud.firestore.TransactionOptions.Builder { - method @NonNull public com.google.cloud.firestore.TransactionOptions build(); + method public com.google.cloud.firestore.TransactionOptions build(); method @Nullable public TimestampOrBuilder getReadTime(); - method @NonNull public com.google.cloud.firestore.TransactionOptions.ReadOnlyOptionsBuilder setReadTime(@Nullable TimestampOrBuilder); + method public com.google.cloud.firestore.TransactionOptions.ReadOnlyOptionsBuilder setReadTime(@Nullable TimestampOrBuilder); } public static final class TransactionOptions.ReadWriteOptionsBuilder extends com.google.cloud.firestore.TransactionOptions.Builder { - method @NonNull public com.google.cloud.firestore.TransactionOptions build(); + method public com.google.cloud.firestore.TransactionOptions build(); method public int getNumberOfAttempts(); - method @NonNull public com.google.cloud.firestore.TransactionOptions.ReadWriteOptionsBuilder setNumberOfAttempts(int); + method public com.google.cloud.firestore.TransactionOptions.ReadWriteOptionsBuilder setNumberOfAttempts(int); } public enum TransactionOptions.TransactionOptionsType { @@ -593,32 +713,38 @@ package com.google.cloud.firestore { public abstract class UpdateBuilder { method protected String className(); - method @NonNull public T create(@NonNull com.google.cloud.firestore.DocumentReference, @NonNull Map); - method @NonNull public T create(@NonNull com.google.cloud.firestore.DocumentReference, @NonNull Object); - method @NonNull public T delete(@NonNull com.google.cloud.firestore.DocumentReference); - method @NonNull public T delete(@NonNull com.google.cloud.firestore.DocumentReference, @NonNull com.google.cloud.firestore.Precondition); + method public T create(com.google.cloud.firestore.DocumentReference, Map); + method public T create(com.google.cloud.firestore.DocumentReference, Object); + method public T delete(com.google.cloud.firestore.DocumentReference); + method public T delete(com.google.cloud.firestore.DocumentReference, com.google.cloud.firestore.Precondition); method public int getMutationsSize(); - method @NonNull public T set(@NonNull com.google.cloud.firestore.DocumentReference, @NonNull Map); - method @NonNull public T set(@NonNull com.google.cloud.firestore.DocumentReference, @NonNull Map, @NonNull com.google.cloud.firestore.SetOptions); - method @NonNull public T set(@NonNull com.google.cloud.firestore.DocumentReference, @NonNull Object); - method @NonNull public T set(@NonNull com.google.cloud.firestore.DocumentReference, @NonNull Object, @NonNull com.google.cloud.firestore.SetOptions); + method public T set(com.google.cloud.firestore.DocumentReference, Map); + method public T set(com.google.cloud.firestore.DocumentReference, Map, com.google.cloud.firestore.SetOptions); + method public T set(com.google.cloud.firestore.DocumentReference, Object); + method public T set(com.google.cloud.firestore.DocumentReference, Object, com.google.cloud.firestore.SetOptions); method public String toString(); - method @NonNull public T update(@NonNull com.google.cloud.firestore.DocumentReference, @NonNull com.google.cloud.firestore.FieldPath, @Nullable Object, Object...); - method @NonNull public T update(@NonNull com.google.cloud.firestore.DocumentReference, @NonNull com.google.cloud.firestore.Precondition, @NonNull com.google.cloud.firestore.FieldPath, @Nullable Object, Object...); - method @NonNull public T update(@NonNull com.google.cloud.firestore.DocumentReference, @NonNull com.google.cloud.firestore.Precondition, @NonNull String, @Nullable Object, Object...); - method @NonNull public T update(@NonNull com.google.cloud.firestore.DocumentReference, @NonNull Map); - method @NonNull public T update(@NonNull com.google.cloud.firestore.DocumentReference, @NonNull Map, com.google.cloud.firestore.Precondition); - method @NonNull public T update(@NonNull com.google.cloud.firestore.DocumentReference, @NonNull String, @Nullable Object, Object...); + method public T update(com.google.cloud.firestore.DocumentReference, com.google.cloud.firestore.FieldPath, @Nullable Object, Object...); + method public T update(com.google.cloud.firestore.DocumentReference, com.google.cloud.firestore.Precondition, com.google.cloud.firestore.FieldPath, @Nullable Object, Object...); + method public T update(com.google.cloud.firestore.DocumentReference, com.google.cloud.firestore.Precondition, String, @Nullable Object, Object...); + method public T update(com.google.cloud.firestore.DocumentReference, Map); + method public T update(com.google.cloud.firestore.DocumentReference, Map, com.google.cloud.firestore.Precondition); + method public T update(com.google.cloud.firestore.DocumentReference, String, @Nullable Object, Object...); field protected volatile boolean committed; } + public final class VectorValue { + method public boolean equals(Object); + method public int hashCode(); + method public double[] toArray(); + } + public class WriteBatch extends com.google.cloud.firestore.UpdateBuilder { - method @NonNull public ApiFuture> commit(); + method public ApiFuture> commit(); } public final class WriteResult { method public boolean equals(Object); - method @NonNull public Timestamp getUpdateTime(); + method public Timestamp getUpdateTime(); method public int hashCode(); } @@ -841,6 +967,693 @@ package com.google.cloud.firestore.collection { } +package com.google.cloud.firestore.pipeline.expressions { + + public class AggregateFunction { + method public com.google.cloud.firestore.pipeline.expressions.AliasedAggregate as(String); + method public static com.google.cloud.firestore.pipeline.expressions.AggregateFunction average(com.google.cloud.firestore.pipeline.expressions.Expression); + method public static com.google.cloud.firestore.pipeline.expressions.AggregateFunction average(String); + method public static com.google.cloud.firestore.pipeline.expressions.AggregateFunction count(com.google.cloud.firestore.pipeline.expressions.Expression); + method public static com.google.cloud.firestore.pipeline.expressions.AggregateFunction count(String); + method public static com.google.cloud.firestore.pipeline.expressions.AggregateFunction countAll(); + method public static com.google.cloud.firestore.pipeline.expressions.AggregateFunction countDistinct(com.google.cloud.firestore.pipeline.expressions.Expression); + method public static com.google.cloud.firestore.pipeline.expressions.AggregateFunction countDistinct(String); + method public static com.google.cloud.firestore.pipeline.expressions.AggregateFunction countIf(com.google.cloud.firestore.pipeline.expressions.BooleanExpression); + method public static com.google.cloud.firestore.pipeline.expressions.AggregateFunction generic(String, com.google.cloud.firestore.pipeline.expressions.Expression...); + method public static com.google.cloud.firestore.pipeline.expressions.AggregateFunction maximum(com.google.cloud.firestore.pipeline.expressions.Expression); + method public static com.google.cloud.firestore.pipeline.expressions.AggregateFunction maximum(String); + method public static com.google.cloud.firestore.pipeline.expressions.AggregateFunction minimum(com.google.cloud.firestore.pipeline.expressions.Expression); + method public static com.google.cloud.firestore.pipeline.expressions.AggregateFunction minimum(String); + method public static com.google.cloud.firestore.pipeline.expressions.AggregateFunction sum(com.google.cloud.firestore.pipeline.expressions.Expression); + method public static com.google.cloud.firestore.pipeline.expressions.AggregateFunction sum(String); + } + + public class AliasedAggregate { + method public String getAlias(); + method public com.google.cloud.firestore.pipeline.expressions.AggregateFunction getExpr(); + } + + public final class AliasedExpression extends com.google.cloud.firestore.pipeline.expressions.Expression implements com.google.cloud.firestore.pipeline.expressions.Selectable { + method public String getAlias(); + method public T getExpr(); + } + + public class BooleanExpression extends com.google.cloud.firestore.pipeline.expressions.FunctionExpression { + } + + public final class Constant extends com.google.cloud.firestore.pipeline.expressions.Expression { + method public static com.google.cloud.firestore.pipeline.expressions.Constant nullValue(); + method public static com.google.cloud.firestore.pipeline.expressions.Constant of(Boolean); + method public static com.google.cloud.firestore.pipeline.expressions.Constant of(byte[]); + method public static com.google.cloud.firestore.pipeline.expressions.Constant of(com.google.cloud.firestore.Blob); + method public static com.google.cloud.firestore.pipeline.expressions.Constant of(com.google.cloud.firestore.DocumentReference); + method public static com.google.cloud.firestore.pipeline.expressions.Constant of(com.google.cloud.firestore.GeoPoint); + method public static com.google.cloud.firestore.pipeline.expressions.Constant of(Date); + method public static com.google.cloud.firestore.pipeline.expressions.Constant of(Iterable); + method public static com.google.cloud.firestore.pipeline.expressions.Constant of(Map); + method public static com.google.cloud.firestore.pipeline.expressions.Constant of(Number); + method public static com.google.cloud.firestore.pipeline.expressions.Constant of(Object[]); + method public static com.google.cloud.firestore.pipeline.expressions.Constant of(String); + method public static com.google.cloud.firestore.pipeline.expressions.Constant of(Timestamp); + method public static com.google.cloud.firestore.pipeline.expressions.Constant of(Value); + method public static com.google.cloud.firestore.pipeline.expressions.Constant vector(double[]); + } + + public abstract class Expression { + method public final com.google.cloud.firestore.pipeline.expressions.Expression abs(); + method public static com.google.cloud.firestore.pipeline.expressions.Expression abs(com.google.cloud.firestore.pipeline.expressions.Expression); + method public static com.google.cloud.firestore.pipeline.expressions.Expression abs(String); + method public static com.google.cloud.firestore.pipeline.expressions.Expression add(com.google.cloud.firestore.pipeline.expressions.Expression, com.google.cloud.firestore.pipeline.expressions.Expression); + method public static com.google.cloud.firestore.pipeline.expressions.Expression add(com.google.cloud.firestore.pipeline.expressions.Expression, Number); + method public final com.google.cloud.firestore.pipeline.expressions.Expression add(Object); + method public static com.google.cloud.firestore.pipeline.expressions.Expression add(String, com.google.cloud.firestore.pipeline.expressions.Expression); + method public static com.google.cloud.firestore.pipeline.expressions.Expression add(String, Number); + method public static com.google.cloud.firestore.pipeline.expressions.BooleanExpression and(com.google.cloud.firestore.pipeline.expressions.BooleanExpression, com.google.cloud.firestore.pipeline.expressions.BooleanExpression...); + method public static com.google.cloud.firestore.pipeline.expressions.Expression array(List); + method public static com.google.cloud.firestore.pipeline.expressions.Expression array(Object...); + method public static com.google.cloud.firestore.pipeline.expressions.Expression arrayConcat(com.google.cloud.firestore.pipeline.expressions.Expression, Object...); + method public final com.google.cloud.firestore.pipeline.expressions.Expression arrayConcat(com.google.cloud.firestore.pipeline.expressions.Expression...); + method public static com.google.cloud.firestore.pipeline.expressions.Expression arrayConcat(String, Object...); + method public static com.google.cloud.firestore.pipeline.expressions.BooleanExpression arrayContains(com.google.cloud.firestore.pipeline.expressions.Expression, com.google.cloud.firestore.pipeline.expressions.Expression); + method public static com.google.cloud.firestore.pipeline.expressions.BooleanExpression arrayContains(com.google.cloud.firestore.pipeline.expressions.Expression, Object); + method public final com.google.cloud.firestore.pipeline.expressions.BooleanExpression arrayContains(Object); + method public static com.google.cloud.firestore.pipeline.expressions.BooleanExpression arrayContains(String, com.google.cloud.firestore.pipeline.expressions.Expression); + method public static com.google.cloud.firestore.pipeline.expressions.BooleanExpression arrayContains(String, Object); + method public final com.google.cloud.firestore.pipeline.expressions.BooleanExpression arrayContainsAll(com.google.cloud.firestore.pipeline.expressions.Expression); + method public static com.google.cloud.firestore.pipeline.expressions.BooleanExpression arrayContainsAll(com.google.cloud.firestore.pipeline.expressions.Expression, com.google.cloud.firestore.pipeline.expressions.Expression); + method public static com.google.cloud.firestore.pipeline.expressions.BooleanExpression arrayContainsAll(com.google.cloud.firestore.pipeline.expressions.Expression, List); + method public final com.google.cloud.firestore.pipeline.expressions.BooleanExpression arrayContainsAll(List); + method public static com.google.cloud.firestore.pipeline.expressions.BooleanExpression arrayContainsAll(String, com.google.cloud.firestore.pipeline.expressions.Expression); + method public static com.google.cloud.firestore.pipeline.expressions.BooleanExpression arrayContainsAll(String, List); + method public final com.google.cloud.firestore.pipeline.expressions.BooleanExpression arrayContainsAny(com.google.cloud.firestore.pipeline.expressions.Expression); + method public static com.google.cloud.firestore.pipeline.expressions.BooleanExpression arrayContainsAny(com.google.cloud.firestore.pipeline.expressions.Expression, com.google.cloud.firestore.pipeline.expressions.Expression); + method public static com.google.cloud.firestore.pipeline.expressions.BooleanExpression arrayContainsAny(com.google.cloud.firestore.pipeline.expressions.Expression, List); + method public final com.google.cloud.firestore.pipeline.expressions.BooleanExpression arrayContainsAny(List); + method public static com.google.cloud.firestore.pipeline.expressions.BooleanExpression arrayContainsAny(String, com.google.cloud.firestore.pipeline.expressions.Expression); + method public static com.google.cloud.firestore.pipeline.expressions.BooleanExpression arrayContainsAny(String, List); + method public final com.google.cloud.firestore.pipeline.expressions.Expression arrayGet(com.google.cloud.firestore.pipeline.expressions.Expression); + method public static com.google.cloud.firestore.pipeline.expressions.Expression arrayGet(com.google.cloud.firestore.pipeline.expressions.Expression, com.google.cloud.firestore.pipeline.expressions.Expression); + method public static com.google.cloud.firestore.pipeline.expressions.Expression arrayGet(com.google.cloud.firestore.pipeline.expressions.Expression, int); + method public final com.google.cloud.firestore.pipeline.expressions.Expression arrayGet(int); + method public static com.google.cloud.firestore.pipeline.expressions.Expression arrayGet(String, com.google.cloud.firestore.pipeline.expressions.Expression); + method public static com.google.cloud.firestore.pipeline.expressions.Expression arrayGet(String, int); + method public final com.google.cloud.firestore.pipeline.expressions.Expression arrayLength(); + method public static com.google.cloud.firestore.pipeline.expressions.Expression arrayLength(com.google.cloud.firestore.pipeline.expressions.Expression); + method public static com.google.cloud.firestore.pipeline.expressions.Expression arrayLength(String); + method public final com.google.cloud.firestore.pipeline.expressions.Expression arrayReverse(); + method public static com.google.cloud.firestore.pipeline.expressions.Expression arrayReverse(com.google.cloud.firestore.pipeline.expressions.Expression); + method public static com.google.cloud.firestore.pipeline.expressions.Expression arrayReverse(String); + method public com.google.cloud.firestore.pipeline.expressions.Selectable as(String); + method public final com.google.cloud.firestore.pipeline.expressions.Ordering ascending(); + method public final com.google.cloud.firestore.pipeline.expressions.AggregateFunction average(); + method public static com.google.cloud.firestore.pipeline.expressions.Expression blob(byte[]); + method public final com.google.cloud.firestore.pipeline.expressions.Expression byteLength(); + method public static com.google.cloud.firestore.pipeline.expressions.Expression byteLength(com.google.cloud.firestore.pipeline.expressions.Expression); + method public static com.google.cloud.firestore.pipeline.expressions.Expression byteLength(String); + method public final com.google.cloud.firestore.pipeline.expressions.Expression ceil(); + method public static com.google.cloud.firestore.pipeline.expressions.Expression ceil(com.google.cloud.firestore.pipeline.expressions.Expression); + method public static com.google.cloud.firestore.pipeline.expressions.Expression ceil(String); + method public final com.google.cloud.firestore.pipeline.expressions.Expression charLength(); + method public static com.google.cloud.firestore.pipeline.expressions.Expression charLength(com.google.cloud.firestore.pipeline.expressions.Expression); + method public static com.google.cloud.firestore.pipeline.expressions.Expression charLength(String); + method public final com.google.cloud.firestore.pipeline.expressions.Expression collectionId(); + method public static com.google.cloud.firestore.pipeline.expressions.Expression collectionId(com.google.cloud.firestore.pipeline.expressions.Expression); + method public static com.google.cloud.firestore.pipeline.expressions.Expression collectionId(String); + method public static com.google.cloud.firestore.pipeline.expressions.Expression conditional(com.google.cloud.firestore.pipeline.expressions.BooleanExpression, com.google.cloud.firestore.pipeline.expressions.Expression, com.google.cloud.firestore.pipeline.expressions.Expression); + method public static com.google.cloud.firestore.pipeline.expressions.Expression conditional(com.google.cloud.firestore.pipeline.expressions.BooleanExpression, Object, Object); + method public final com.google.cloud.firestore.pipeline.expressions.Expression conditional(com.google.cloud.firestore.pipeline.expressions.Expression, com.google.cloud.firestore.pipeline.expressions.Expression); + method public final com.google.cloud.firestore.pipeline.expressions.Expression conditional(Object, Object); + method public static com.google.cloud.firestore.pipeline.expressions.BooleanExpression constant(Boolean); + method public static com.google.cloud.firestore.pipeline.expressions.Expression constant(byte[]); + method public static com.google.cloud.firestore.pipeline.expressions.Expression constant(com.google.cloud.firestore.Blob); + method public static com.google.cloud.firestore.pipeline.expressions.Expression constant(com.google.cloud.firestore.DocumentReference); + method public static com.google.cloud.firestore.pipeline.expressions.Expression constant(com.google.cloud.firestore.GeoPoint); + method public static com.google.cloud.firestore.pipeline.expressions.Expression constant(com.google.cloud.firestore.VectorValue); + method public static com.google.cloud.firestore.pipeline.expressions.Expression constant(Date); + method public static com.google.cloud.firestore.pipeline.expressions.Expression constant(Number); + method public static com.google.cloud.firestore.pipeline.expressions.Expression constant(String); + method public static com.google.cloud.firestore.pipeline.expressions.Expression constant(Timestamp); + method public final com.google.cloud.firestore.pipeline.expressions.Expression cosineDistance(com.google.cloud.firestore.pipeline.expressions.Expression); + method public static com.google.cloud.firestore.pipeline.expressions.Expression cosineDistance(com.google.cloud.firestore.pipeline.expressions.Expression, com.google.cloud.firestore.pipeline.expressions.Expression); + method public static com.google.cloud.firestore.pipeline.expressions.Expression cosineDistance(com.google.cloud.firestore.pipeline.expressions.Expression, double[]); + method public final com.google.cloud.firestore.pipeline.expressions.Expression cosineDistance(double[]); + method public static com.google.cloud.firestore.pipeline.expressions.Expression cosineDistance(String, com.google.cloud.firestore.pipeline.expressions.Expression); + method public static com.google.cloud.firestore.pipeline.expressions.Expression cosineDistance(String, double[]); + method public final com.google.cloud.firestore.pipeline.expressions.AggregateFunction count(); + method public final com.google.cloud.firestore.pipeline.expressions.AggregateFunction countDistinct(); + method public final com.google.cloud.firestore.pipeline.expressions.Ordering descending(); + method public static com.google.cloud.firestore.pipeline.expressions.Expression divide(com.google.cloud.firestore.pipeline.expressions.Expression, com.google.cloud.firestore.pipeline.expressions.Expression); + method public static com.google.cloud.firestore.pipeline.expressions.Expression divide(com.google.cloud.firestore.pipeline.expressions.Expression, Number); + method public final com.google.cloud.firestore.pipeline.expressions.Expression divide(Object); + method public static com.google.cloud.firestore.pipeline.expressions.Expression divide(String, com.google.cloud.firestore.pipeline.expressions.Expression); + method public static com.google.cloud.firestore.pipeline.expressions.Expression divide(String, Number); + method public final com.google.cloud.firestore.pipeline.expressions.Expression documentId(); + method public static com.google.cloud.firestore.pipeline.expressions.Expression documentId(com.google.cloud.firestore.DocumentReference); + method public static com.google.cloud.firestore.pipeline.expressions.Expression documentId(com.google.cloud.firestore.pipeline.expressions.Expression); + method public static com.google.cloud.firestore.pipeline.expressions.Expression documentId(String); + method public final com.google.cloud.firestore.pipeline.expressions.Expression dotProduct(com.google.cloud.firestore.pipeline.expressions.Expression); + method public static com.google.cloud.firestore.pipeline.expressions.Expression dotProduct(com.google.cloud.firestore.pipeline.expressions.Expression, com.google.cloud.firestore.pipeline.expressions.Expression); + method public static com.google.cloud.firestore.pipeline.expressions.Expression dotProduct(com.google.cloud.firestore.pipeline.expressions.Expression, double[]); + method public final com.google.cloud.firestore.pipeline.expressions.Expression dotProduct(double[]); + method public static com.google.cloud.firestore.pipeline.expressions.Expression dotProduct(String, com.google.cloud.firestore.pipeline.expressions.Expression); + method public static com.google.cloud.firestore.pipeline.expressions.Expression dotProduct(String, double[]); + method public static com.google.cloud.firestore.pipeline.expressions.BooleanExpression endsWith(com.google.cloud.firestore.pipeline.expressions.Expression, com.google.cloud.firestore.pipeline.expressions.Expression); + method public static com.google.cloud.firestore.pipeline.expressions.BooleanExpression endsWith(com.google.cloud.firestore.pipeline.expressions.Expression, String); + method public final com.google.cloud.firestore.pipeline.expressions.BooleanExpression endsWith(Object); + method public static com.google.cloud.firestore.pipeline.expressions.BooleanExpression endsWith(String, com.google.cloud.firestore.pipeline.expressions.Expression); + method public static com.google.cloud.firestore.pipeline.expressions.BooleanExpression endsWith(String, String); + method public static com.google.cloud.firestore.pipeline.expressions.BooleanExpression equal(com.google.cloud.firestore.pipeline.expressions.Expression, com.google.cloud.firestore.pipeline.expressions.Expression); + method public static com.google.cloud.firestore.pipeline.expressions.BooleanExpression equal(com.google.cloud.firestore.pipeline.expressions.Expression, Object); + method public final com.google.cloud.firestore.pipeline.expressions.BooleanExpression equal(Object); + method public static com.google.cloud.firestore.pipeline.expressions.BooleanExpression equal(String, com.google.cloud.firestore.pipeline.expressions.Expression); + method public static com.google.cloud.firestore.pipeline.expressions.BooleanExpression equal(String, Object); + method public static com.google.cloud.firestore.pipeline.expressions.BooleanExpression equalAny(com.google.cloud.firestore.pipeline.expressions.Expression, com.google.cloud.firestore.pipeline.expressions.Expression); + method public static com.google.cloud.firestore.pipeline.expressions.BooleanExpression equalAny(com.google.cloud.firestore.pipeline.expressions.Expression, List); + method public final com.google.cloud.firestore.pipeline.expressions.BooleanExpression equalAny(List); + method public static com.google.cloud.firestore.pipeline.expressions.BooleanExpression equalAny(String, com.google.cloud.firestore.pipeline.expressions.Expression); + method public static com.google.cloud.firestore.pipeline.expressions.BooleanExpression equalAny(String, List); + method public final com.google.cloud.firestore.pipeline.expressions.Expression euclideanDistance(com.google.cloud.firestore.pipeline.expressions.Expression); + method public static com.google.cloud.firestore.pipeline.expressions.Expression euclideanDistance(com.google.cloud.firestore.pipeline.expressions.Expression, com.google.cloud.firestore.pipeline.expressions.Expression); + method public static com.google.cloud.firestore.pipeline.expressions.Expression euclideanDistance(com.google.cloud.firestore.pipeline.expressions.Expression, double[]); + method public final com.google.cloud.firestore.pipeline.expressions.Expression euclideanDistance(double[]); + method public static com.google.cloud.firestore.pipeline.expressions.Expression euclideanDistance(String, com.google.cloud.firestore.pipeline.expressions.Expression); + method public static com.google.cloud.firestore.pipeline.expressions.Expression euclideanDistance(String, double[]); + method public final com.google.cloud.firestore.pipeline.expressions.BooleanExpression exists(); + method public static com.google.cloud.firestore.pipeline.expressions.BooleanExpression exists(com.google.cloud.firestore.pipeline.expressions.Expression); + method public static com.google.cloud.firestore.pipeline.expressions.BooleanExpression exists(String); + method public final com.google.cloud.firestore.pipeline.expressions.Expression exp(); + method public static com.google.cloud.firestore.pipeline.expressions.Expression exp(com.google.cloud.firestore.pipeline.expressions.Expression); + method public static com.google.cloud.firestore.pipeline.expressions.Expression exp(String); + method public static com.google.cloud.firestore.pipeline.expressions.Field field(com.google.cloud.firestore.FieldPath); + method public static com.google.cloud.firestore.pipeline.expressions.Field field(String); + method public final com.google.cloud.firestore.pipeline.expressions.Expression floor(); + method public static com.google.cloud.firestore.pipeline.expressions.Expression floor(com.google.cloud.firestore.pipeline.expressions.Expression); + method public static com.google.cloud.firestore.pipeline.expressions.Expression floor(String); + method public static com.google.cloud.firestore.pipeline.expressions.Expression generic(String, com.google.cloud.firestore.pipeline.expressions.Expression...); + method public static com.google.cloud.firestore.pipeline.expressions.BooleanExpression greaterThan(com.google.cloud.firestore.pipeline.expressions.Expression, com.google.cloud.firestore.pipeline.expressions.Expression); + method public static com.google.cloud.firestore.pipeline.expressions.BooleanExpression greaterThan(com.google.cloud.firestore.pipeline.expressions.Expression, Object); + method public final com.google.cloud.firestore.pipeline.expressions.BooleanExpression greaterThan(Object); + method public static com.google.cloud.firestore.pipeline.expressions.BooleanExpression greaterThan(String, com.google.cloud.firestore.pipeline.expressions.Expression); + method public static com.google.cloud.firestore.pipeline.expressions.BooleanExpression greaterThan(String, Object); + method public static com.google.cloud.firestore.pipeline.expressions.BooleanExpression greaterThanOrEqual(com.google.cloud.firestore.pipeline.expressions.Expression, com.google.cloud.firestore.pipeline.expressions.Expression); + method public static com.google.cloud.firestore.pipeline.expressions.BooleanExpression greaterThanOrEqual(com.google.cloud.firestore.pipeline.expressions.Expression, Object); + method public final com.google.cloud.firestore.pipeline.expressions.BooleanExpression greaterThanOrEqual(Object); + method public static com.google.cloud.firestore.pipeline.expressions.BooleanExpression greaterThanOrEqual(String, com.google.cloud.firestore.pipeline.expressions.Expression); + method public static com.google.cloud.firestore.pipeline.expressions.BooleanExpression greaterThanOrEqual(String, Object); + method public static com.google.cloud.firestore.pipeline.expressions.BooleanExpression ifError(com.google.cloud.firestore.pipeline.expressions.BooleanExpression, com.google.cloud.firestore.pipeline.expressions.BooleanExpression); + method public final com.google.cloud.firestore.pipeline.expressions.Expression ifError(com.google.cloud.firestore.pipeline.expressions.Expression); + method public static com.google.cloud.firestore.pipeline.expressions.Expression ifError(com.google.cloud.firestore.pipeline.expressions.Expression, com.google.cloud.firestore.pipeline.expressions.Expression); + method public static com.google.cloud.firestore.pipeline.expressions.Expression ifError(com.google.cloud.firestore.pipeline.expressions.Expression, Object); + method public final com.google.cloud.firestore.pipeline.expressions.Expression ifError(Object); + method public final com.google.cloud.firestore.pipeline.expressions.BooleanExpression isAbsent(); + method public static com.google.cloud.firestore.pipeline.expressions.BooleanExpression isAbsent(com.google.cloud.firestore.pipeline.expressions.Expression); + method public static com.google.cloud.firestore.pipeline.expressions.BooleanExpression isAbsent(String); + method public final com.google.cloud.firestore.pipeline.expressions.BooleanExpression isError(); + method public static com.google.cloud.firestore.pipeline.expressions.BooleanExpression isError(com.google.cloud.firestore.pipeline.expressions.Expression); + method public final com.google.cloud.firestore.pipeline.expressions.BooleanExpression isNaN(); + method public static com.google.cloud.firestore.pipeline.expressions.BooleanExpression isNaN(com.google.cloud.firestore.pipeline.expressions.Expression); + method public static com.google.cloud.firestore.pipeline.expressions.BooleanExpression isNaN(String); + method public final com.google.cloud.firestore.pipeline.expressions.BooleanExpression isNotNaN(); + method public static com.google.cloud.firestore.pipeline.expressions.BooleanExpression isNotNaN(com.google.cloud.firestore.pipeline.expressions.Expression); + method public static com.google.cloud.firestore.pipeline.expressions.BooleanExpression isNotNaN(String); + method public final com.google.cloud.firestore.pipeline.expressions.BooleanExpression isNotNull(); + method public static com.google.cloud.firestore.pipeline.expressions.BooleanExpression isNotNull(com.google.cloud.firestore.pipeline.expressions.Expression); + method public static com.google.cloud.firestore.pipeline.expressions.BooleanExpression isNotNull(String); + method public final com.google.cloud.firestore.pipeline.expressions.BooleanExpression isNull(); + method public static com.google.cloud.firestore.pipeline.expressions.BooleanExpression isNull(com.google.cloud.firestore.pipeline.expressions.Expression); + method public static com.google.cloud.firestore.pipeline.expressions.BooleanExpression isNull(String); + method public final com.google.cloud.firestore.pipeline.expressions.Expression length(); + method public static com.google.cloud.firestore.pipeline.expressions.Expression length(com.google.cloud.firestore.pipeline.expressions.Expression); + method public static com.google.cloud.firestore.pipeline.expressions.Expression length(String); + method public static com.google.cloud.firestore.pipeline.expressions.BooleanExpression lessThan(com.google.cloud.firestore.pipeline.expressions.Expression, com.google.cloud.firestore.pipeline.expressions.Expression); + method public static com.google.cloud.firestore.pipeline.expressions.BooleanExpression lessThan(com.google.cloud.firestore.pipeline.expressions.Expression, Object); + method public final com.google.cloud.firestore.pipeline.expressions.BooleanExpression lessThan(Object); + method public static com.google.cloud.firestore.pipeline.expressions.BooleanExpression lessThan(String, com.google.cloud.firestore.pipeline.expressions.Expression); + method public static com.google.cloud.firestore.pipeline.expressions.BooleanExpression lessThan(String, Object); + method public static com.google.cloud.firestore.pipeline.expressions.BooleanExpression lessThanOrEqual(com.google.cloud.firestore.pipeline.expressions.Expression, com.google.cloud.firestore.pipeline.expressions.Expression); + method public static com.google.cloud.firestore.pipeline.expressions.BooleanExpression lessThanOrEqual(com.google.cloud.firestore.pipeline.expressions.Expression, Object); + method public final com.google.cloud.firestore.pipeline.expressions.BooleanExpression lessThanOrEqual(Object); + method public static com.google.cloud.firestore.pipeline.expressions.BooleanExpression lessThanOrEqual(String, com.google.cloud.firestore.pipeline.expressions.Expression); + method public static com.google.cloud.firestore.pipeline.expressions.BooleanExpression lessThanOrEqual(String, Object); + method public static com.google.cloud.firestore.pipeline.expressions.BooleanExpression like(com.google.cloud.firestore.pipeline.expressions.Expression, com.google.cloud.firestore.pipeline.expressions.Expression); + method public static com.google.cloud.firestore.pipeline.expressions.BooleanExpression like(com.google.cloud.firestore.pipeline.expressions.Expression, String); + method public final com.google.cloud.firestore.pipeline.expressions.BooleanExpression like(Object); + method public static com.google.cloud.firestore.pipeline.expressions.BooleanExpression like(String, com.google.cloud.firestore.pipeline.expressions.Expression); + method public static com.google.cloud.firestore.pipeline.expressions.BooleanExpression like(String, String); + method public final com.google.cloud.firestore.pipeline.expressions.Expression ln(); + method public static com.google.cloud.firestore.pipeline.expressions.Expression ln(com.google.cloud.firestore.pipeline.expressions.Expression); + method public static com.google.cloud.firestore.pipeline.expressions.Expression ln(String); + method public final com.google.cloud.firestore.pipeline.expressions.Expression log(com.google.cloud.firestore.pipeline.expressions.Expression); + method public static com.google.cloud.firestore.pipeline.expressions.Expression log(com.google.cloud.firestore.pipeline.expressions.Expression, com.google.cloud.firestore.pipeline.expressions.Expression); + method public static com.google.cloud.firestore.pipeline.expressions.Expression log(com.google.cloud.firestore.pipeline.expressions.Expression, Number); + method public final com.google.cloud.firestore.pipeline.expressions.Expression log(Number); + method public static com.google.cloud.firestore.pipeline.expressions.Expression log(String, com.google.cloud.firestore.pipeline.expressions.Expression); + method public static com.google.cloud.firestore.pipeline.expressions.Expression log(String, Number); + method public final com.google.cloud.firestore.pipeline.expressions.Expression log10(); + method public static com.google.cloud.firestore.pipeline.expressions.Expression log10(com.google.cloud.firestore.pipeline.expressions.Expression); + method public static com.google.cloud.firestore.pipeline.expressions.Expression log10(String); + method public static com.google.cloud.firestore.pipeline.expressions.Expression logicalMaximum(com.google.cloud.firestore.pipeline.expressions.Expression, Object...); + method public final com.google.cloud.firestore.pipeline.expressions.Expression logicalMaximum(Object...); + method public static com.google.cloud.firestore.pipeline.expressions.Expression logicalMaximum(String, Object...); + method public static com.google.cloud.firestore.pipeline.expressions.Expression logicalMinimum(com.google.cloud.firestore.pipeline.expressions.Expression, Object...); + method public final com.google.cloud.firestore.pipeline.expressions.Expression logicalMinimum(Object...); + method public static com.google.cloud.firestore.pipeline.expressions.Expression logicalMinimum(String, Object...); + method public static com.google.cloud.firestore.pipeline.expressions.Expression map(Map); + method public static com.google.cloud.firestore.pipeline.expressions.Expression mapGet(com.google.cloud.firestore.pipeline.expressions.Expression, com.google.cloud.firestore.pipeline.expressions.Expression); + method public static com.google.cloud.firestore.pipeline.expressions.Expression mapGet(com.google.cloud.firestore.pipeline.expressions.Expression, String); + method public final com.google.cloud.firestore.pipeline.expressions.Expression mapGet(Object); + method public static com.google.cloud.firestore.pipeline.expressions.Expression mapGet(String, com.google.cloud.firestore.pipeline.expressions.Expression); + method public static com.google.cloud.firestore.pipeline.expressions.Expression mapGet(String, String); + method public static com.google.cloud.firestore.pipeline.expressions.Expression mapMerge(com.google.cloud.firestore.pipeline.expressions.Expression, com.google.cloud.firestore.pipeline.expressions.Expression); + method public static com.google.cloud.firestore.pipeline.expressions.Expression mapMerge(com.google.cloud.firestore.pipeline.expressions.Expression, com.google.cloud.firestore.pipeline.expressions.Expression, com.google.cloud.firestore.pipeline.expressions.Expression...); + method public final com.google.cloud.firestore.pipeline.expressions.Expression mapMerge(com.google.cloud.firestore.pipeline.expressions.Expression, com.google.cloud.firestore.pipeline.expressions.Expression...); + method public static com.google.cloud.firestore.pipeline.expressions.Expression mapMerge(String, com.google.cloud.firestore.pipeline.expressions.Expression); + method public static com.google.cloud.firestore.pipeline.expressions.Expression mapMerge(String, com.google.cloud.firestore.pipeline.expressions.Expression, com.google.cloud.firestore.pipeline.expressions.Expression...); + method public final com.google.cloud.firestore.pipeline.expressions.Expression mapRemove(com.google.cloud.firestore.pipeline.expressions.Expression); + method public static com.google.cloud.firestore.pipeline.expressions.Expression mapRemove(com.google.cloud.firestore.pipeline.expressions.Expression, com.google.cloud.firestore.pipeline.expressions.Expression); + method public static com.google.cloud.firestore.pipeline.expressions.Expression mapRemove(com.google.cloud.firestore.pipeline.expressions.Expression, String); + method public final com.google.cloud.firestore.pipeline.expressions.Expression mapRemove(String); + method public static com.google.cloud.firestore.pipeline.expressions.Expression mapRemove(String, com.google.cloud.firestore.pipeline.expressions.Expression); + method public static com.google.cloud.firestore.pipeline.expressions.Expression mapRemove(String, String); + method public final com.google.cloud.firestore.pipeline.expressions.AggregateFunction maximum(); + method public final com.google.cloud.firestore.pipeline.expressions.AggregateFunction minimum(); + method public static com.google.cloud.firestore.pipeline.expressions.Expression mod(com.google.cloud.firestore.pipeline.expressions.Expression, com.google.cloud.firestore.pipeline.expressions.Expression); + method public static com.google.cloud.firestore.pipeline.expressions.Expression mod(com.google.cloud.firestore.pipeline.expressions.Expression, Number); + method public final com.google.cloud.firestore.pipeline.expressions.Expression mod(Object); + method public static com.google.cloud.firestore.pipeline.expressions.Expression mod(String, com.google.cloud.firestore.pipeline.expressions.Expression); + method public static com.google.cloud.firestore.pipeline.expressions.Expression mod(String, Number); + method public static com.google.cloud.firestore.pipeline.expressions.Expression multiply(com.google.cloud.firestore.pipeline.expressions.Expression, com.google.cloud.firestore.pipeline.expressions.Expression); + method public static com.google.cloud.firestore.pipeline.expressions.Expression multiply(com.google.cloud.firestore.pipeline.expressions.Expression, Number); + method public final com.google.cloud.firestore.pipeline.expressions.Expression multiply(Object); + method public static com.google.cloud.firestore.pipeline.expressions.Expression multiply(String, com.google.cloud.firestore.pipeline.expressions.Expression); + method public static com.google.cloud.firestore.pipeline.expressions.Expression multiply(String, Number); + method public static com.google.cloud.firestore.pipeline.expressions.BooleanExpression not(com.google.cloud.firestore.pipeline.expressions.BooleanExpression); + method public static com.google.cloud.firestore.pipeline.expressions.BooleanExpression notEqual(com.google.cloud.firestore.pipeline.expressions.Expression, com.google.cloud.firestore.pipeline.expressions.Expression); + method public static com.google.cloud.firestore.pipeline.expressions.BooleanExpression notEqual(com.google.cloud.firestore.pipeline.expressions.Expression, Object); + method public final com.google.cloud.firestore.pipeline.expressions.BooleanExpression notEqual(Object); + method public static com.google.cloud.firestore.pipeline.expressions.BooleanExpression notEqual(String, com.google.cloud.firestore.pipeline.expressions.Expression); + method public static com.google.cloud.firestore.pipeline.expressions.BooleanExpression notEqual(String, Object); + method public static com.google.cloud.firestore.pipeline.expressions.BooleanExpression notEqualAny(com.google.cloud.firestore.pipeline.expressions.Expression, com.google.cloud.firestore.pipeline.expressions.Expression); + method public static com.google.cloud.firestore.pipeline.expressions.BooleanExpression notEqualAny(com.google.cloud.firestore.pipeline.expressions.Expression, List); + method public final com.google.cloud.firestore.pipeline.expressions.BooleanExpression notEqualAny(List); + method public static com.google.cloud.firestore.pipeline.expressions.BooleanExpression notEqualAny(String, com.google.cloud.firestore.pipeline.expressions.Expression); + method public static com.google.cloud.firestore.pipeline.expressions.BooleanExpression notEqualAny(String, List); + method public static com.google.cloud.firestore.pipeline.expressions.Expression nullValue(); + method public static com.google.cloud.firestore.pipeline.expressions.BooleanExpression or(com.google.cloud.firestore.pipeline.expressions.BooleanExpression, com.google.cloud.firestore.pipeline.expressions.BooleanExpression...); + method public final com.google.cloud.firestore.pipeline.expressions.Expression pow(com.google.cloud.firestore.pipeline.expressions.Expression); + method public static com.google.cloud.firestore.pipeline.expressions.Expression pow(com.google.cloud.firestore.pipeline.expressions.Expression, com.google.cloud.firestore.pipeline.expressions.Expression); + method public static com.google.cloud.firestore.pipeline.expressions.Expression pow(com.google.cloud.firestore.pipeline.expressions.Expression, Number); + method public final com.google.cloud.firestore.pipeline.expressions.Expression pow(Number); + method public static com.google.cloud.firestore.pipeline.expressions.Expression pow(String, com.google.cloud.firestore.pipeline.expressions.Expression); + method public static com.google.cloud.firestore.pipeline.expressions.Expression pow(String, Number); + method public static com.google.cloud.firestore.pipeline.expressions.Expression rand(); + method public static com.google.cloud.firestore.pipeline.expressions.BooleanExpression regexContains(com.google.cloud.firestore.pipeline.expressions.Expression, com.google.cloud.firestore.pipeline.expressions.Expression); + method public static com.google.cloud.firestore.pipeline.expressions.BooleanExpression regexContains(com.google.cloud.firestore.pipeline.expressions.Expression, String); + method public final com.google.cloud.firestore.pipeline.expressions.BooleanExpression regexContains(Object); + method public static com.google.cloud.firestore.pipeline.expressions.BooleanExpression regexContains(String, com.google.cloud.firestore.pipeline.expressions.Expression); + method public static com.google.cloud.firestore.pipeline.expressions.BooleanExpression regexContains(String, String); + method public static com.google.cloud.firestore.pipeline.expressions.BooleanExpression regexMatch(com.google.cloud.firestore.pipeline.expressions.Expression, com.google.cloud.firestore.pipeline.expressions.Expression); + method public static com.google.cloud.firestore.pipeline.expressions.BooleanExpression regexMatch(com.google.cloud.firestore.pipeline.expressions.Expression, String); + method public final com.google.cloud.firestore.pipeline.expressions.BooleanExpression regexMatch(Object); + method public static com.google.cloud.firestore.pipeline.expressions.BooleanExpression regexMatch(String, com.google.cloud.firestore.pipeline.expressions.Expression); + method public static com.google.cloud.firestore.pipeline.expressions.BooleanExpression regexMatch(String, String); + method public final com.google.cloud.firestore.pipeline.expressions.Expression round(); + method public static com.google.cloud.firestore.pipeline.expressions.Expression round(com.google.cloud.firestore.pipeline.expressions.Expression); + method public static com.google.cloud.firestore.pipeline.expressions.Expression round(String); + method public final com.google.cloud.firestore.pipeline.expressions.Expression roundToPrecision(com.google.cloud.firestore.pipeline.expressions.Expression); + method public static com.google.cloud.firestore.pipeline.expressions.Expression roundToPrecision(com.google.cloud.firestore.pipeline.expressions.Expression, com.google.cloud.firestore.pipeline.expressions.Expression); + method public static com.google.cloud.firestore.pipeline.expressions.Expression roundToPrecision(com.google.cloud.firestore.pipeline.expressions.Expression, int); + method public final com.google.cloud.firestore.pipeline.expressions.Expression roundToPrecision(int); + method public static com.google.cloud.firestore.pipeline.expressions.Expression roundToPrecision(String, com.google.cloud.firestore.pipeline.expressions.Expression); + method public static com.google.cloud.firestore.pipeline.expressions.Expression roundToPrecision(String, int); + method public final com.google.cloud.firestore.pipeline.expressions.Expression sqrt(); + method public static com.google.cloud.firestore.pipeline.expressions.Expression sqrt(com.google.cloud.firestore.pipeline.expressions.Expression); + method public static com.google.cloud.firestore.pipeline.expressions.Expression sqrt(String); + method public static com.google.cloud.firestore.pipeline.expressions.BooleanExpression startsWith(com.google.cloud.firestore.pipeline.expressions.Expression, com.google.cloud.firestore.pipeline.expressions.Expression); + method public static com.google.cloud.firestore.pipeline.expressions.BooleanExpression startsWith(com.google.cloud.firestore.pipeline.expressions.Expression, String); + method public final com.google.cloud.firestore.pipeline.expressions.BooleanExpression startsWith(Object); + method public static com.google.cloud.firestore.pipeline.expressions.BooleanExpression startsWith(String, com.google.cloud.firestore.pipeline.expressions.Expression); + method public static com.google.cloud.firestore.pipeline.expressions.BooleanExpression startsWith(String, String); + method public static com.google.cloud.firestore.pipeline.expressions.Expression stringConcat(com.google.cloud.firestore.pipeline.expressions.Expression, Object...); + method public final com.google.cloud.firestore.pipeline.expressions.Expression stringConcat(com.google.cloud.firestore.pipeline.expressions.Expression...); + method public static com.google.cloud.firestore.pipeline.expressions.Expression stringConcat(String, Object...); + method public final com.google.cloud.firestore.pipeline.expressions.Expression stringConcat(String...); + method public static com.google.cloud.firestore.pipeline.expressions.BooleanExpression stringContains(com.google.cloud.firestore.pipeline.expressions.Expression, com.google.cloud.firestore.pipeline.expressions.Expression); + method public static com.google.cloud.firestore.pipeline.expressions.BooleanExpression stringContains(com.google.cloud.firestore.pipeline.expressions.Expression, String); + method public final com.google.cloud.firestore.pipeline.expressions.BooleanExpression stringContains(Object); + method public static com.google.cloud.firestore.pipeline.expressions.BooleanExpression stringContains(String, com.google.cloud.firestore.pipeline.expressions.Expression); + method public static com.google.cloud.firestore.pipeline.expressions.BooleanExpression stringContains(String, String); + method public static com.google.cloud.firestore.pipeline.expressions.Expression substring(com.google.cloud.firestore.pipeline.expressions.Expression, com.google.cloud.firestore.pipeline.expressions.Expression, com.google.cloud.firestore.pipeline.expressions.Expression); + method public final com.google.cloud.firestore.pipeline.expressions.Expression substring(Object, Object); + method public static com.google.cloud.firestore.pipeline.expressions.Expression substring(String, int, int); + method public static com.google.cloud.firestore.pipeline.expressions.Expression subtract(com.google.cloud.firestore.pipeline.expressions.Expression, com.google.cloud.firestore.pipeline.expressions.Expression); + method public static com.google.cloud.firestore.pipeline.expressions.Expression subtract(com.google.cloud.firestore.pipeline.expressions.Expression, Number); + method public final com.google.cloud.firestore.pipeline.expressions.Expression subtract(Object); + method public static com.google.cloud.firestore.pipeline.expressions.Expression subtract(String, com.google.cloud.firestore.pipeline.expressions.Expression); + method public static com.google.cloud.firestore.pipeline.expressions.Expression subtract(String, Number); + method public final com.google.cloud.firestore.pipeline.expressions.AggregateFunction sum(); + method public final com.google.cloud.firestore.pipeline.expressions.Expression timestampAdd(com.google.cloud.firestore.pipeline.expressions.Expression, com.google.cloud.firestore.pipeline.expressions.Expression); + method public static com.google.cloud.firestore.pipeline.expressions.Expression timestampAdd(com.google.cloud.firestore.pipeline.expressions.Expression, com.google.cloud.firestore.pipeline.expressions.Expression, com.google.cloud.firestore.pipeline.expressions.Expression); + method public static com.google.cloud.firestore.pipeline.expressions.Expression timestampAdd(com.google.cloud.firestore.pipeline.expressions.Expression, String, long); + method public static com.google.cloud.firestore.pipeline.expressions.Expression timestampAdd(String, com.google.cloud.firestore.pipeline.expressions.Expression, com.google.cloud.firestore.pipeline.expressions.Expression); + method public final com.google.cloud.firestore.pipeline.expressions.Expression timestampAdd(String, long); + method public static com.google.cloud.firestore.pipeline.expressions.Expression timestampAdd(String, String, long); + method public final com.google.cloud.firestore.pipeline.expressions.Expression timestampSubtract(com.google.cloud.firestore.pipeline.expressions.Expression, com.google.cloud.firestore.pipeline.expressions.Expression); + method public static com.google.cloud.firestore.pipeline.expressions.Expression timestampSubtract(com.google.cloud.firestore.pipeline.expressions.Expression, com.google.cloud.firestore.pipeline.expressions.Expression, com.google.cloud.firestore.pipeline.expressions.Expression); + method public static com.google.cloud.firestore.pipeline.expressions.Expression timestampSubtract(com.google.cloud.firestore.pipeline.expressions.Expression, String, long); + method public static com.google.cloud.firestore.pipeline.expressions.Expression timestampSubtract(String, com.google.cloud.firestore.pipeline.expressions.Expression, com.google.cloud.firestore.pipeline.expressions.Expression); + method public final com.google.cloud.firestore.pipeline.expressions.Expression timestampSubtract(String, long); + method public static com.google.cloud.firestore.pipeline.expressions.Expression timestampSubtract(String, String, long); + method public final com.google.cloud.firestore.pipeline.expressions.Expression timestampToUnixMicros(); + method public static com.google.cloud.firestore.pipeline.expressions.Expression timestampToUnixMicros(com.google.cloud.firestore.pipeline.expressions.Expression); + method public static com.google.cloud.firestore.pipeline.expressions.Expression timestampToUnixMicros(String); + method public final com.google.cloud.firestore.pipeline.expressions.Expression timestampToUnixMillis(); + method public static com.google.cloud.firestore.pipeline.expressions.Expression timestampToUnixMillis(com.google.cloud.firestore.pipeline.expressions.Expression); + method public static com.google.cloud.firestore.pipeline.expressions.Expression timestampToUnixMillis(String); + method public final com.google.cloud.firestore.pipeline.expressions.Expression timestampToUnixSeconds(); + method public static com.google.cloud.firestore.pipeline.expressions.Expression timestampToUnixSeconds(com.google.cloud.firestore.pipeline.expressions.Expression); + method public static com.google.cloud.firestore.pipeline.expressions.Expression timestampToUnixSeconds(String); + method public final com.google.cloud.firestore.pipeline.expressions.Expression toLower(); + method public static com.google.cloud.firestore.pipeline.expressions.Expression toLower(com.google.cloud.firestore.pipeline.expressions.Expression); + method public static com.google.cloud.firestore.pipeline.expressions.Expression toLower(String); + method public final com.google.cloud.firestore.pipeline.expressions.Expression toUpper(); + method public static com.google.cloud.firestore.pipeline.expressions.Expression toUpper(com.google.cloud.firestore.pipeline.expressions.Expression); + method public static com.google.cloud.firestore.pipeline.expressions.Expression toUpper(String); + method public final com.google.cloud.firestore.pipeline.expressions.Expression trim(); + method public static com.google.cloud.firestore.pipeline.expressions.Expression trim(com.google.cloud.firestore.pipeline.expressions.Expression); + method public static com.google.cloud.firestore.pipeline.expressions.Expression trim(String); + method public final com.google.cloud.firestore.pipeline.expressions.Expression unixMicrosToTimestamp(); + method public static com.google.cloud.firestore.pipeline.expressions.Expression unixMicrosToTimestamp(com.google.cloud.firestore.pipeline.expressions.Expression); + method public static com.google.cloud.firestore.pipeline.expressions.Expression unixMicrosToTimestamp(String); + method public final com.google.cloud.firestore.pipeline.expressions.Expression unixMillisToTimestamp(); + method public static com.google.cloud.firestore.pipeline.expressions.Expression unixMillisToTimestamp(com.google.cloud.firestore.pipeline.expressions.Expression); + method public static com.google.cloud.firestore.pipeline.expressions.Expression unixMillisToTimestamp(String); + method public final com.google.cloud.firestore.pipeline.expressions.Expression unixSecondsToTimestamp(); + method public static com.google.cloud.firestore.pipeline.expressions.Expression unixSecondsToTimestamp(com.google.cloud.firestore.pipeline.expressions.Expression); + method public static com.google.cloud.firestore.pipeline.expressions.Expression unixSecondsToTimestamp(String); + method public static com.google.cloud.firestore.pipeline.expressions.Expression vector(com.google.cloud.firestore.VectorValue); + method public static com.google.cloud.firestore.pipeline.expressions.Expression vector(double[]); + method public final com.google.cloud.firestore.pipeline.expressions.Expression vectorLength(); + method public static com.google.cloud.firestore.pipeline.expressions.Expression vectorLength(com.google.cloud.firestore.pipeline.expressions.Expression); + method public static com.google.cloud.firestore.pipeline.expressions.Expression vectorLength(String); + method public static com.google.cloud.firestore.pipeline.expressions.BooleanExpression xor(com.google.cloud.firestore.pipeline.expressions.BooleanExpression, com.google.cloud.firestore.pipeline.expressions.BooleanExpression...); + } + + public final class Field extends com.google.cloud.firestore.pipeline.expressions.Expression implements com.google.cloud.firestore.pipeline.expressions.Selectable { + method public boolean equals(Object); + method public com.google.cloud.firestore.FieldPath getPath(); + method public int hashCode(); + method public static com.google.cloud.firestore.pipeline.expressions.Field ofServerPath(String); + method public static com.google.cloud.firestore.pipeline.expressions.Field ofUserPath(String); + method public Value toProto(); + field public static final String DOCUMENT_ID = "__name__"; + } + + public class FunctionExpression extends com.google.cloud.firestore.pipeline.expressions.Expression { + method public boolean equals(Object); + method public int hashCode(); + } + + public final class FunctionUtils { + ctor public FunctionUtils(); + method public static Value aggregateFunctionToValue(com.google.cloud.firestore.pipeline.expressions.AggregateFunction); + method public static Value exprToValue(com.google.cloud.firestore.pipeline.expressions.Expression); + } + + public final class Ordering { + method public static com.google.cloud.firestore.pipeline.expressions.Ordering ascending(com.google.cloud.firestore.pipeline.expressions.Expression); + method public static com.google.cloud.firestore.pipeline.expressions.Ordering descending(com.google.cloud.firestore.pipeline.expressions.Expression); + method public com.google.cloud.firestore.pipeline.expressions.Ordering.Direction getDir(); + method public com.google.cloud.firestore.pipeline.expressions.Expression getExpr(); + method public Value toProto(); + } + + public enum Ordering.Direction { + method public String toString(); + enum_constant public static final com.google.cloud.firestore.pipeline.expressions.Ordering.Direction ASCENDING; + enum_constant public static final com.google.cloud.firestore.pipeline.expressions.Ordering.Direction DESCENDING; + } + + public interface Selectable { + } + +} + +package com.google.cloud.firestore.pipeline.stages { + + public final class AddFields extends com.google.cloud.firestore.pipeline.stages.Stage { + ctor public AddFields(Map); + } + + public final class Aggregate extends com.google.cloud.firestore.pipeline.stages.Stage { + method public static com.google.cloud.firestore.pipeline.stages.Aggregate withAccumulators(com.google.cloud.firestore.pipeline.expressions.AliasedAggregate...); + method public com.google.cloud.firestore.pipeline.stages.Aggregate withGroups(com.google.cloud.firestore.pipeline.expressions.Selectable...); + method public com.google.cloud.firestore.pipeline.stages.Aggregate withGroups(String...); + method public com.google.cloud.firestore.pipeline.stages.Aggregate withOptions(com.google.cloud.firestore.pipeline.stages.AggregateOptions); + } + + public final class AggregateHints { + ctor public AggregateHints(); + method public final com.google.cloud.firestore.pipeline.stages.AggregateHints with(String, boolean); + method public final com.google.cloud.firestore.pipeline.stages.AggregateHints with(String, com.google.cloud.firestore.pipeline.expressions.Field); + method public final com.google.cloud.firestore.pipeline.stages.AggregateHints with(String, double); + method public final com.google.cloud.firestore.pipeline.stages.AggregateHints with(String, long); + method public final com.google.cloud.firestore.pipeline.stages.AggregateHints with(String, String); + method public com.google.cloud.firestore.pipeline.stages.AggregateHints withForceStreamableEnabled(); + method public final com.google.cloud.firestore.pipeline.stages.AggregateHints withSection(String, com.google.cloud.firestore.pipeline.stages.RawOptions); + } + + public final class AggregateOptions { + ctor public AggregateOptions(); + method public final com.google.cloud.firestore.pipeline.stages.AggregateOptions with(String, boolean); + method public final com.google.cloud.firestore.pipeline.stages.AggregateOptions with(String, com.google.cloud.firestore.pipeline.expressions.Field); + method public final com.google.cloud.firestore.pipeline.stages.AggregateOptions with(String, double); + method public final com.google.cloud.firestore.pipeline.stages.AggregateOptions with(String, long); + method public final com.google.cloud.firestore.pipeline.stages.AggregateOptions with(String, String); + method public com.google.cloud.firestore.pipeline.stages.AggregateOptions withHints(com.google.cloud.firestore.pipeline.stages.AggregateHints); + method public final com.google.cloud.firestore.pipeline.stages.AggregateOptions withSection(String, com.google.cloud.firestore.pipeline.stages.RawOptions); + } + + public final class Collection extends com.google.cloud.firestore.pipeline.stages.Stage { + ctor public Collection(String, com.google.cloud.firestore.pipeline.stages.CollectionOptions); + method public com.google.cloud.firestore.pipeline.stages.Collection withOptions(com.google.cloud.firestore.pipeline.stages.CollectionOptions); + } + + public final class CollectionGroup extends com.google.cloud.firestore.pipeline.stages.Stage { + ctor public CollectionGroup(String, com.google.cloud.firestore.pipeline.stages.CollectionGroupOptions); + method public com.google.cloud.firestore.pipeline.stages.CollectionGroup withOptions(com.google.cloud.firestore.pipeline.stages.CollectionGroupOptions); + } + + public final class CollectionGroupOptions { + ctor public CollectionGroupOptions(); + method public final com.google.cloud.firestore.pipeline.stages.CollectionGroupOptions with(String, boolean); + method public final com.google.cloud.firestore.pipeline.stages.CollectionGroupOptions with(String, com.google.cloud.firestore.pipeline.expressions.Field); + method public final com.google.cloud.firestore.pipeline.stages.CollectionGroupOptions with(String, double); + method public final com.google.cloud.firestore.pipeline.stages.CollectionGroupOptions with(String, long); + method public final com.google.cloud.firestore.pipeline.stages.CollectionGroupOptions with(String, String); + method public com.google.cloud.firestore.pipeline.stages.CollectionGroupOptions withHints(com.google.cloud.firestore.pipeline.stages.CollectionHints); + method public final com.google.cloud.firestore.pipeline.stages.CollectionGroupOptions withSection(String, com.google.cloud.firestore.pipeline.stages.RawOptions); + } + + public final class CollectionHints { + ctor public CollectionHints(); + method public final com.google.cloud.firestore.pipeline.stages.CollectionHints with(String, boolean); + method public final com.google.cloud.firestore.pipeline.stages.CollectionHints with(String, com.google.cloud.firestore.pipeline.expressions.Field); + method public final com.google.cloud.firestore.pipeline.stages.CollectionHints with(String, double); + method public final com.google.cloud.firestore.pipeline.stages.CollectionHints with(String, long); + method public final com.google.cloud.firestore.pipeline.stages.CollectionHints with(String, String); + method public com.google.cloud.firestore.pipeline.stages.CollectionHints withForceIndex(String); + method public com.google.cloud.firestore.pipeline.stages.CollectionHints withIgnoreIndexFields(String...); + method public final com.google.cloud.firestore.pipeline.stages.CollectionHints withSection(String, com.google.cloud.firestore.pipeline.stages.RawOptions); + } + + public final class CollectionOptions { + ctor public CollectionOptions(); + method public final com.google.cloud.firestore.pipeline.stages.CollectionOptions with(String, boolean); + method public final com.google.cloud.firestore.pipeline.stages.CollectionOptions with(String, com.google.cloud.firestore.pipeline.expressions.Field); + method public final com.google.cloud.firestore.pipeline.stages.CollectionOptions with(String, double); + method public final com.google.cloud.firestore.pipeline.stages.CollectionOptions with(String, long); + method public final com.google.cloud.firestore.pipeline.stages.CollectionOptions with(String, String); + method public com.google.cloud.firestore.pipeline.stages.CollectionOptions withHints(com.google.cloud.firestore.pipeline.stages.CollectionHints); + method public final com.google.cloud.firestore.pipeline.stages.CollectionOptions withSection(String, com.google.cloud.firestore.pipeline.stages.RawOptions); + } + + public final class Database extends com.google.cloud.firestore.pipeline.stages.Stage { + ctor public Database(); + } + + public final class Distinct extends com.google.cloud.firestore.pipeline.stages.Stage { + ctor public Distinct(Map); + } + + public final class Documents extends com.google.cloud.firestore.pipeline.stages.Stage { + method public static com.google.cloud.firestore.pipeline.stages.Documents of(com.google.cloud.firestore.DocumentReference...); + } + + public final class ExplainOptions { + ctor public ExplainOptions(); + method public final com.google.cloud.firestore.pipeline.stages.ExplainOptions with(String, boolean); + method public final com.google.cloud.firestore.pipeline.stages.ExplainOptions with(String, com.google.cloud.firestore.pipeline.expressions.Field); + method public final com.google.cloud.firestore.pipeline.stages.ExplainOptions with(String, double); + method public final com.google.cloud.firestore.pipeline.stages.ExplainOptions with(String, long); + method public final com.google.cloud.firestore.pipeline.stages.ExplainOptions with(String, String); + method public com.google.cloud.firestore.pipeline.stages.ExplainOptions withExecutionMode(com.google.cloud.firestore.pipeline.stages.ExplainOptions.ExecutionMode); + method public final com.google.cloud.firestore.pipeline.stages.ExplainOptions withSection(String, com.google.cloud.firestore.pipeline.stages.RawOptions); + } + + public static class ExplainOptions.ExecutionMode { + method public Value toProto(); + field public static final com.google.cloud.firestore.pipeline.stages.ExplainOptions.ExecutionMode ANALYZE; + field public static final com.google.cloud.firestore.pipeline.stages.ExplainOptions.ExecutionMode EXPLAIN; + } + + public final class FindNearest extends com.google.cloud.firestore.pipeline.stages.Stage { + ctor public FindNearest(com.google.cloud.firestore.pipeline.expressions.Expression, com.google.cloud.firestore.VectorValue, com.google.cloud.firestore.pipeline.stages.FindNearest.DistanceMeasure, com.google.cloud.firestore.pipeline.stages.FindNearestOptions); + } + + public static final class FindNearest.DistanceMeasure { + method public static com.google.cloud.firestore.pipeline.stages.FindNearest.DistanceMeasure generic(String); + field public static final com.google.cloud.firestore.pipeline.stages.FindNearest.DistanceMeasure COSINE; + field public static final com.google.cloud.firestore.pipeline.stages.FindNearest.DistanceMeasure DOT_PRODUCT; + field public static final com.google.cloud.firestore.pipeline.stages.FindNearest.DistanceMeasure EUCLIDEAN; + } + + public final class FindNearestOptions { + ctor public FindNearestOptions(); + method public final com.google.cloud.firestore.pipeline.stages.FindNearestOptions with(String, boolean); + method public final com.google.cloud.firestore.pipeline.stages.FindNearestOptions with(String, com.google.cloud.firestore.pipeline.expressions.Field); + method public final com.google.cloud.firestore.pipeline.stages.FindNearestOptions with(String, double); + method public final com.google.cloud.firestore.pipeline.stages.FindNearestOptions with(String, long); + method public final com.google.cloud.firestore.pipeline.stages.FindNearestOptions with(String, String); + method public com.google.cloud.firestore.pipeline.stages.FindNearestOptions withDistanceField(com.google.cloud.firestore.pipeline.expressions.Field); + method public com.google.cloud.firestore.pipeline.stages.FindNearestOptions withDistanceField(String); + method public com.google.cloud.firestore.pipeline.stages.FindNearestOptions withLimit(long); + method public final com.google.cloud.firestore.pipeline.stages.FindNearestOptions withSection(String, com.google.cloud.firestore.pipeline.stages.RawOptions); + } + + public final class GenericOptions { + ctor public GenericOptions(); + method public static com.google.cloud.firestore.pipeline.stages.RawOptions of(String, boolean); + method public static com.google.cloud.firestore.pipeline.stages.RawOptions of(String, com.google.cloud.firestore.pipeline.expressions.Field); + method public static com.google.cloud.firestore.pipeline.stages.RawOptions of(String, double); + method public static com.google.cloud.firestore.pipeline.stages.RawOptions of(String, long); + method public static com.google.cloud.firestore.pipeline.stages.RawOptions of(String, String); + method protected com.google.cloud.firestore.pipeline.stages.RawOptions self(com.google.cloud.firestore.pipeline.stages.InternalOptions); + method public final com.google.cloud.firestore.pipeline.stages.RawOptions with(String, boolean); + method public final com.google.cloud.firestore.pipeline.stages.RawOptions with(String, com.google.cloud.firestore.pipeline.expressions.Field); + method public final com.google.cloud.firestore.pipeline.stages.RawOptions with(String, double); + method public final com.google.cloud.firestore.pipeline.stages.RawOptions with(String, long); + method public final com.google.cloud.firestore.pipeline.stages.RawOptions with(String, String); + method public final com.google.cloud.firestore.pipeline.stages.RawOptions withSection(String, com.google.cloud.firestore.pipeline.stages.RawOptions); + } + + public final class GenericStage extends com.google.cloud.firestore.pipeline.stages.Stage { + ctor public GenericStage(String, List, com.google.cloud.firestore.pipeline.stages.RawOptions); + } + + public final class Limit extends com.google.cloud.firestore.pipeline.stages.Stage { + ctor public Limit(int); + } + + public final class Offset extends com.google.cloud.firestore.pipeline.stages.Stage { + ctor public Offset(int); + } + + public final class PipelineExecuteOptions { + ctor public PipelineExecuteOptions(); + method public final com.google.cloud.firestore.pipeline.stages.PipelineExecuteOptions with(String, boolean); + method public final com.google.cloud.firestore.pipeline.stages.PipelineExecuteOptions with(String, com.google.cloud.firestore.pipeline.expressions.Field); + method public final com.google.cloud.firestore.pipeline.stages.PipelineExecuteOptions with(String, double); + method public final com.google.cloud.firestore.pipeline.stages.PipelineExecuteOptions with(String, long); + method public final com.google.cloud.firestore.pipeline.stages.PipelineExecuteOptions with(String, String); + method public com.google.cloud.firestore.pipeline.stages.PipelineExecuteOptions withExplainOptions(com.google.cloud.firestore.pipeline.stages.ExplainOptions); + method public com.google.cloud.firestore.pipeline.stages.PipelineExecuteOptions withIndexMode(String); + method public final com.google.cloud.firestore.pipeline.stages.PipelineExecuteOptions withSection(String, com.google.cloud.firestore.pipeline.stages.RawOptions); + } + + public final class RemoveFields extends com.google.cloud.firestore.pipeline.stages.Stage { + ctor public RemoveFields(ImmutableList); + } + + public class ReplaceWith extends com.google.cloud.firestore.pipeline.stages.Stage { + ctor public ReplaceWith(com.google.cloud.firestore.pipeline.expressions.Expression); + ctor public ReplaceWith(com.google.cloud.firestore.pipeline.expressions.Expression, com.google.cloud.firestore.pipeline.stages.ReplaceWith.Mode); + } + + public enum ReplaceWith.Mode { + enum_constant public static final com.google.cloud.firestore.pipeline.stages.ReplaceWith.Mode FULL_REPLACE; + enum_constant public static final com.google.cloud.firestore.pipeline.stages.ReplaceWith.Mode MERGE_PREFER_NEXT; + enum_constant public static final com.google.cloud.firestore.pipeline.stages.ReplaceWith.Mode MERGE_PREFER_PARENT; + field public final Value value; + } + + public final class Sample extends com.google.cloud.firestore.pipeline.stages.Stage { + method public static com.google.cloud.firestore.pipeline.stages.Sample withDocLimit(int); + method public com.google.cloud.firestore.pipeline.stages.Sample withOptions(com.google.cloud.firestore.pipeline.stages.SampleOptions); + method public static com.google.cloud.firestore.pipeline.stages.Sample withPercentage(double); + } + + public enum Sample.Mode { + enum_constant public static final com.google.cloud.firestore.pipeline.stages.Sample.Mode DOCUMENTS; + enum_constant public static final com.google.cloud.firestore.pipeline.stages.Sample.Mode PERCENT; + field public final Value value; + } + + public final class SampleOptions { + ctor public SampleOptions(); + method public final com.google.cloud.firestore.pipeline.stages.SampleOptions with(String, boolean); + method public final com.google.cloud.firestore.pipeline.stages.SampleOptions with(String, com.google.cloud.firestore.pipeline.expressions.Field); + method public final com.google.cloud.firestore.pipeline.stages.SampleOptions with(String, double); + method public final com.google.cloud.firestore.pipeline.stages.SampleOptions with(String, long); + method public final com.google.cloud.firestore.pipeline.stages.SampleOptions with(String, String); + method public final com.google.cloud.firestore.pipeline.stages.SampleOptions withSection(String, com.google.cloud.firestore.pipeline.stages.RawOptions); + } + + public final class Select extends com.google.cloud.firestore.pipeline.stages.Stage { + ctor public Select(Map); + } + + public final class Sort extends com.google.cloud.firestore.pipeline.stages.Stage { + ctor public Sort(ImmutableList); + } + + public abstract class Stage { + field protected final String name; + } + + public final class StageUtils { + ctor public StageUtils(); + method public static ImmutableMap toMap(com.google.cloud.firestore.pipeline.stages.AbstractOptions); + method public static com.google.firestore.v1.Pipeline.Stage toStageProto(com.google.cloud.firestore.pipeline.stages.Stage); + } + + public final class Union extends com.google.cloud.firestore.pipeline.stages.Stage { + ctor public Union(com.google.cloud.firestore.Pipeline); + } + + public final class Unnest extends com.google.cloud.firestore.pipeline.stages.Stage { + ctor public Unnest(com.google.cloud.firestore.pipeline.expressions.Field, String); + ctor public Unnest(com.google.cloud.firestore.pipeline.expressions.Field, String, com.google.cloud.firestore.pipeline.stages.UnnestOptions); + ctor public Unnest(com.google.cloud.firestore.pipeline.expressions.Selectable); + } + + public final class UnnestOptions { + ctor public UnnestOptions(); + method public final com.google.cloud.firestore.pipeline.stages.UnnestOptions with(String, boolean); + method public final com.google.cloud.firestore.pipeline.stages.UnnestOptions with(String, com.google.cloud.firestore.pipeline.expressions.Field); + method public final com.google.cloud.firestore.pipeline.stages.UnnestOptions with(String, double); + method public final com.google.cloud.firestore.pipeline.stages.UnnestOptions with(String, long); + method public final com.google.cloud.firestore.pipeline.stages.UnnestOptions with(String, String); + method public com.google.cloud.firestore.pipeline.stages.UnnestOptions withIndexField(String); + method public final com.google.cloud.firestore.pipeline.stages.UnnestOptions withSection(String, com.google.cloud.firestore.pipeline.stages.RawOptions); + } + + public final class Where extends com.google.cloud.firestore.pipeline.stages.Stage { + ctor public Where(com.google.cloud.firestore.pipeline.expressions.BooleanExpression); + } + +} + package com.google.cloud.firestore.spi.v1 { public interface FirestoreRpc { @@ -848,6 +1661,7 @@ package com.google.cloud.firestore.spi.v1 { method public UnaryCallable batchWriteCallable(); method public UnaryCallable beginTransactionCallable(); method public UnaryCallable commitCallable(); + method public ServerStreamingCallable executePipelineCallable(); method public ScheduledExecutorService getExecutor(); method public UnaryCallable listCollectionIdsPagedCallable(); method public UnaryCallable listDocumentsPagedCallable(); @@ -867,6 +1681,7 @@ package com.google.cloud.firestore.spi.v1 { method public UnaryCallable beginTransactionCallable(); method public void close(); method public UnaryCallable commitCallable(); + method public ServerStreamingCallable executePipelineCallable(); method public ScheduledExecutorService getExecutor(); method public UnaryCallable listCollectionIdsPagedCallable(); method public UnaryCallable listDocumentsPagedCallable(); @@ -881,6 +1696,97 @@ package com.google.cloud.firestore.spi.v1 { } +package com.google.cloud.firestore.telemetry { + + public class DisabledTraceUtil implements com.google.cloud.firestore.telemetry.TraceUtil { + ctor public DisabledTraceUtil(); + method public com.google.cloud.firestore.telemetry.TraceUtil.Context currentContext(); + method public com.google.cloud.firestore.telemetry.TraceUtil.Span currentSpan(); + method @Nullable public ApiFunction getChannelConfigurator(); + method public com.google.cloud.firestore.telemetry.DisabledTraceUtil.Span startSpan(String); + method public com.google.cloud.firestore.telemetry.TraceUtil.Span startSpan(String, com.google.cloud.firestore.telemetry.TraceUtil.Context); + } + + public class EnabledTraceUtil implements com.google.cloud.firestore.telemetry.TraceUtil { + method public com.google.cloud.firestore.telemetry.TraceUtil.Context currentContext(); + method public com.google.cloud.firestore.telemetry.TraceUtil.Span currentSpan(); + method @Nullable public ApiFunction getChannelConfigurator(); + method public OpenTelemetry getOpenTelemetry(); + method public com.google.cloud.firestore.telemetry.EnabledTraceUtil.Span startSpan(String); + method public com.google.cloud.firestore.telemetry.TraceUtil.Span startSpan(String, com.google.cloud.firestore.telemetry.TraceUtil.Context); + } + + public class EnabledTraceUtil.OpenTelemetryGrpcChannelConfigurator { + ctor public EnabledTraceUtil.OpenTelemetryGrpcChannelConfigurator(); + method public ManagedChannelBuilder apply(ManagedChannelBuilder); + } + + public interface TraceUtil { + method public com.google.cloud.firestore.telemetry.TraceUtil.Context currentContext(); + method public com.google.cloud.firestore.telemetry.TraceUtil.Span currentSpan(); + method @Nullable public ApiFunction getChannelConfigurator(); + method public static com.google.cloud.firestore.telemetry.TraceUtil getInstance(com.google.cloud.firestore.FirestoreOptions); + method public com.google.cloud.firestore.telemetry.TraceUtil.Span startSpan(String); + method public com.google.cloud.firestore.telemetry.TraceUtil.Span startSpan(String, com.google.cloud.firestore.telemetry.TraceUtil.Context); + field public static final String ATTRIBUTE_KEY_ATTEMPT = "attempt"; + field public static final String ATTRIBUTE_KEY_ATTEMPTS_ALLOWED = "attempts_allowed"; + field public static final String ATTRIBUTE_KEY_ATTEMPTS_REMAINING = "attempts_remaining"; + field public static final String ATTRIBUTE_KEY_DOC_COUNT = "doc_count"; + field public static final String ATTRIBUTE_KEY_IS_RETRY_WITH_CURSOR = "retry_query_with_cursor"; + field public static final String ATTRIBUTE_KEY_IS_TRANSACTIONAL = "transactional"; + field public static final String ATTRIBUTE_KEY_NUM_RESPONSES = "response_count"; + field public static final String ATTRIBUTE_KEY_TRANSACTION_TYPE = "transaction_type"; + field public static final String ATTRIBUTE_SERVICE_PREFIX = "gcp.firestore."; + field public static final String ENABLE_TRACING_ENV_VAR = "FIRESTORE_ENABLE_TRACING"; + field public static final String LIBRARY_NAME = "com.google.cloud.firestore"; + field public static final String SPAN_NAME_AGGREGATION_QUERY_GET = "AggregationQuery.Get"; + field public static final String SPAN_NAME_BATCH_COMMIT = "Batch.Commit"; + field public static final String SPAN_NAME_BATCH_GET_DOCUMENTS = "BatchGetDocuments"; + field public static final String SPAN_NAME_BULK_WRITER_COMMIT = "BulkWriter.Commit"; + field public static final String SPAN_NAME_COL_REF_ADD = "CollectionReference.Add"; + field public static final String SPAN_NAME_COL_REF_LIST_DOCUMENTS = "CollectionReference.ListDocuments"; + field public static final String SPAN_NAME_DOC_REF_CREATE = "DocumentReference.Create"; + field public static final String SPAN_NAME_DOC_REF_DELETE = "DocumentReference.Delete"; + field public static final String SPAN_NAME_DOC_REF_GET = "DocumentReference.Get"; + field public static final String SPAN_NAME_DOC_REF_LIST_COLLECTIONS = "DocumentReference.ListCollections"; + field public static final String SPAN_NAME_DOC_REF_SET = "DocumentReference.Set"; + field public static final String SPAN_NAME_DOC_REF_UPDATE = "DocumentReference.Update"; + field public static final String SPAN_NAME_PARTITION_QUERY = "PartitionQuery"; + field public static final String SPAN_NAME_QUERY_GET = "Query.Get"; + field public static final String SPAN_NAME_RUN_AGGREGATION_QUERY = "RunAggregationQuery"; + field public static final String SPAN_NAME_RUN_QUERY = "RunQuery"; + field public static final String SPAN_NAME_TRANSACTION_BEGIN = "Transaction.Begin"; + field public static final String SPAN_NAME_TRANSACTION_COMMIT = "Transaction.Commit"; + field public static final String SPAN_NAME_TRANSACTION_GET_AGGREGATION_QUERY = "Transaction.Get.AggregationQuery"; + field public static final String SPAN_NAME_TRANSACTION_GET_DOCUMENT = "Transaction.Get.Document"; + field public static final String SPAN_NAME_TRANSACTION_GET_DOCUMENTS = "Transaction.Get.Documents"; + field public static final String SPAN_NAME_TRANSACTION_GET_QUERY = "Transaction.Get.Query"; + field public static final String SPAN_NAME_TRANSACTION_ROLLBACK = "Transaction.Rollback"; + field public static final String SPAN_NAME_TRANSACTION_RUN = "Transaction.Run"; + } + + public static interface TraceUtil.Context { + method public com.google.cloud.firestore.telemetry.TraceUtil.Scope makeCurrent(); + } + + public static interface TraceUtil.Scope { + method public void close(); + } + + public static interface TraceUtil.Span { + method public com.google.cloud.firestore.telemetry.TraceUtil.Span addEvent(String); + method public com.google.cloud.firestore.telemetry.TraceUtil.Span addEvent(String, Map); + method public void end(); + method public void end(Throwable); + method public void endAtFuture(ApiFuture); + method public com.google.cloud.firestore.telemetry.TraceUtil.Scope makeCurrent(); + method public com.google.cloud.firestore.telemetry.TraceUtil.Span setAttribute(String, boolean); + method public com.google.cloud.firestore.telemetry.TraceUtil.Span setAttribute(String, int); + method public com.google.cloud.firestore.telemetry.TraceUtil.Span setAttribute(String, String); + } + +} + package com.google.cloud.firestore.v1 { public class FirestoreClient { @@ -905,6 +1811,7 @@ package com.google.cloud.firestore.v1 { method public final void deleteDocument(DeleteDocumentRequest); method public final void deleteDocument(String); method public final UnaryCallable deleteDocumentCallable(); + method public final ServerStreamingCallable executePipelineCallable(); method public final Document getDocument(GetDocumentRequest); method public final UnaryCallable getDocumentCallable(); method public final com.google.cloud.firestore.v1.FirestoreSettings getSettings(); @@ -989,6 +1896,7 @@ package com.google.cloud.firestore.v1 { method public static InstantiatingHttpJsonChannelProvider.Builder defaultHttpJsonTransportProviderBuilder(); method public static TransportChannelProvider defaultTransportChannelProvider(); method public UnaryCallSettings deleteDocumentSettings(); + method public ServerStreamingCallSettings executePipelineSettings(); method public static String getDefaultEndpoint(); method public static List getDefaultServiceScopes(); method public UnaryCallSettings getDocumentSettings(); @@ -1020,6 +1928,7 @@ package com.google.cloud.firestore.v1 { method public UnaryCallSettings.Builder commitSettings(); method public UnaryCallSettings.Builder createDocumentSettings(); method public UnaryCallSettings.Builder deleteDocumentSettings(); + method public ServerStreamingCallSettings.Builder executePipelineSettings(); method public UnaryCallSettings.Builder getDocumentSettings(); method public com.google.cloud.firestore.v1.stub.FirestoreStubSettings.Builder getStubSettingsBuilder(); method public PagedCallSettings.Builder listCollectionIdsSettings(); @@ -1046,6 +1955,7 @@ package com.google.cloud.firestore.v1.stub { method public UnaryCallable commitCallable(); method public UnaryCallable createDocumentCallable(); method public UnaryCallable deleteDocumentCallable(); + method public ServerStreamingCallable executePipelineCallable(); method public UnaryCallable getDocumentCallable(); method public UnaryCallable listCollectionIdsCallable(); method public UnaryCallable listCollectionIdsPagedCallable(); @@ -1078,6 +1988,7 @@ package com.google.cloud.firestore.v1.stub { method public static InstantiatingHttpJsonChannelProvider.Builder defaultHttpJsonTransportProviderBuilder(); method public static TransportChannelProvider defaultTransportChannelProvider(); method public UnaryCallSettings deleteDocumentSettings(); + method public ServerStreamingCallSettings executePipelineSettings(); method public static String getDefaultEndpoint(); method public static String getDefaultMtlsEndpoint(); method public static List getDefaultServiceScopes(); @@ -1110,6 +2021,7 @@ package com.google.cloud.firestore.v1.stub { method public UnaryCallSettings.Builder commitSettings(); method public UnaryCallSettings.Builder createDocumentSettings(); method public UnaryCallSettings.Builder deleteDocumentSettings(); + method public ServerStreamingCallSettings.Builder executePipelineSettings(); method public UnaryCallSettings.Builder getDocumentSettings(); method public PagedCallSettings.Builder listCollectionIdsSettings(); method public PagedCallSettings.Builder listDocumentsSettings(); diff --git a/tools/pom.xml b/tools/pom.xml index 4688ffb40..0df5c2208 100644 --- a/tools/pom.xml +++ b/tools/pom.xml @@ -24,9 +24,14 @@ com.android.tools.metalava metalava - 1.0.0-alpha12 + 1.0.0-alpha13 runtime + + com.google.code.findbugs + jsr305 + 3.0.2 +