From e5e0d13fef188339e215e442d2cdd1067231fff5 Mon Sep 17 00:00:00 2001 From: Tom Andersen Date: Mon, 16 Dec 2024 11:09:47 -0500 Subject: [PATCH 1/7] Incomplete prototyping of pipeline options --- .../com/google/cloud/firestore/Pipeline.java | 124 ++++++++++++------ .../cloud/firestore/PipelineSource.java | 20 ++- .../google/cloud/firestore/PipelineUtils.java | 22 ++++ .../cloud/firestore/ReadTimeTransaction.java | 3 +- .../firestore/ServerSideTransaction.java | 3 +- .../pipeline/stages/AbstractOptions.java | 66 ++++++++++ .../firestore/pipeline/stages/AddFields.java | 9 +- .../firestore/pipeline/stages/Aggregate.java | 38 +++--- .../pipeline/stages/AggregateOptions.java | 31 +++++ .../firestore/pipeline/stages/Collection.java | 18 +-- .../pipeline/stages/CollectionGroup.java | 20 +-- .../stages/CollectionGroupOptions.java | 37 ++++++ .../pipeline/stages/CollectionOptions.java | 56 ++++++++ .../firestore/pipeline/stages/Database.java | 12 +- .../firestore/pipeline/stages/Distinct.java | 9 +- .../firestore/pipeline/stages/Documents.java | 14 +- .../pipeline/stages/ExecuteOptions.java | 31 +++++ .../pipeline/stages/FindNearest.java | 90 +++---------- .../pipeline/stages/FindNearestOptions.java | 46 ++----- .../pipeline/stages/GenericOptions.java | 47 +++++++ .../pipeline/stages/GenericStage.java | 17 +-- .../pipeline/stages/InternalOptions.java | 59 +++++++++ .../firestore/pipeline/stages/Limit.java | 9 +- .../firestore/pipeline/stages/Offset.java | 9 +- .../pipeline/stages/RemoveFields.java | 13 +- .../firestore/pipeline/stages/Replace.java | 12 +- .../firestore/pipeline/stages/Sample.java | 47 ++++++- .../firestore/pipeline/stages/Select.java | 9 +- .../cloud/firestore/pipeline/stages/Sort.java | 18 ++- .../firestore/pipeline/stages/Stage.java | 21 ++- .../firestore/pipeline/stages/StageUtils.java | 8 ++ .../firestore/pipeline/stages/Union.java | 10 +- .../firestore/pipeline/stages/Unnest.java | 26 ++-- .../pipeline/stages/UnnestOptions.java | 17 ++- .../firestore/pipeline/stages/Where.java | 9 +- .../cloud/firestore/it/ITPipelineTest.java | 9 +- 36 files changed, 691 insertions(+), 298 deletions(-) create mode 100644 google-cloud-firestore/src/main/java/com/google/cloud/firestore/pipeline/stages/AbstractOptions.java create mode 100644 google-cloud-firestore/src/main/java/com/google/cloud/firestore/pipeline/stages/AggregateOptions.java create mode 100644 google-cloud-firestore/src/main/java/com/google/cloud/firestore/pipeline/stages/CollectionGroupOptions.java create mode 100644 google-cloud-firestore/src/main/java/com/google/cloud/firestore/pipeline/stages/CollectionOptions.java create mode 100644 google-cloud-firestore/src/main/java/com/google/cloud/firestore/pipeline/stages/ExecuteOptions.java create mode 100644 google-cloud-firestore/src/main/java/com/google/cloud/firestore/pipeline/stages/GenericOptions.java create mode 100644 google-cloud-firestore/src/main/java/com/google/cloud/firestore/pipeline/stages/InternalOptions.java 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 index c8cb17cc0..e10dafbc8 100644 --- 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 @@ -25,6 +25,12 @@ 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.stages.AggregateOptions; +import com.google.cloud.firestore.pipeline.stages.Collection; +import com.google.cloud.firestore.pipeline.stages.CollectionOptions; +import com.google.cloud.firestore.pipeline.stages.ExecuteOptions; +import com.google.cloud.firestore.pipeline.stages.FindNearest.DistanceMeasure; +import com.google.cloud.firestore.pipeline.stages.GenericOptions; import com.google.cloud.firestore.pipeline.expressions.Accumulator; import com.google.cloud.firestore.pipeline.expressions.Expr; import com.google.cloud.firestore.pipeline.expressions.ExprWithAlias; @@ -69,6 +75,7 @@ import java.util.List; import java.util.logging.Level; import java.util.logging.Logger; +import javax.annotation.Nonnull; import javax.annotation.Nullable; /** @@ -436,6 +443,34 @@ public Pipeline aggregate(Aggregate aggregate) { return append(aggregate); } + + /** + * 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 ExprWithAlias} expressions which are typically results of + * calling {@link Expr#as(String)} on {@link Accumulator} instances. + * + *

Example: + * + *

{@code
+   * // Calculate the average rating and the total number of books
+   * firestore.pipeline().collection("books")
+   *     .aggregate(
+   *         Field.of("rating").avg().as("averageRating"),
+   *         countAll().as("totalBooks")
+   *     );
+   * }
+ * + * @param accumulators The {@link ExprWithAlias} expressions, each wrapping an {@link Accumulator} + * 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(AggregateOptions options, ExprWithAlias... accumulators) { + return append(Aggregate.withAccumulators(accumulators).withOptions(options)); + } + /** * Returns a set of distinct field values from the inputs to this stage. * @@ -540,8 +575,9 @@ public Pipeline findNearest( *
{@code
    * // Find books with similar "topicVectors" to the given targetVector
    * firestore.pipeline().collection("books")
-   *     .findNearest(Field.of("topicVectors"), targetVector, FindNearest.DistanceMeasure.cosine(),
-   *        FindNearestOptions
+   *     .findNearest(
+   *        FindNearest.of(Field.of("topicVectors"), targetVector, FindNearest.DistanceMeasure.cosine()),
+   *        FindNearest
    *          .builder()
    *          .limit(10)
    *          .distanceField("distance")
@@ -590,7 +626,7 @@ public Pipeline findNearest(
    */
   @BetaApi
   public Pipeline sort(Ordering... orders) {
-    return append(new Sort(orders));
+    return append(new Sort(ImmutableList.copyOf(orders)));
   }
 
   /**
@@ -683,8 +719,7 @@ public Pipeline replace(Selectable field) {
    */
   @BetaApi
   public Pipeline sample(int limit) {
-    SampleOptions options = SampleOptions.docLimit(limit);
-    return sample(options);
+    return sample(Sample.withDocLimit(limit));
   }
 
   /**
@@ -698,19 +733,19 @@ public Pipeline sample(int limit) {
    * 
{@code
    * // Sample 10 books, if available.
    * firestore.pipeline().collection("books")
-   *     .sample(SampleOptions.docLimit(10));
+   *     .sample(Sample.withDocLimit(10));
    *
    * // Sample 50% of books.
    * firestore.pipeline().collection("books")
-   *     .sample(SampleOptions.percentage(0.5));
+   *     .sample(Sample.withPercentage(0.5));
    * }
* - * @param options The {@code SampleOptions} specifies how sampling is performed. + * @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(SampleOptions options) { - return append(new Sample(options)); + public Pipeline sample(Sample sample) { + return append(sample); } /** @@ -756,21 +791,21 @@ public Pipeline union(Pipeline other) { * * // Emit a book document for each tag of the book. * firestore.pipeline().collection("books") - * .unnest("tags"); + * .unnest("tags", "tag"); * * // Output: - * // { "title": "The Hitchhiker's Guide to the Galaxy", "tags": "comedy", ... } - * // { "title": "The Hitchhiker's Guide to the Galaxy", "tags": "space", ... } - * // { "title": "The Hitchhiker's Guide to the Galaxy", "tags": "adventure", ... } + * // { "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) { + public Pipeline unnest(String fieldName, String alias) { // return unnest(Field.of(fieldName)); - return append(new Unnest(Field.of(fieldName))); + return append(new Unnest(Field.of(fieldName), alias)); } // /** @@ -829,12 +864,12 @@ public Pipeline unnest(String fieldName) { * * // Emit a book document for each tag of the book. * firestore.pipeline().collection("books") - * .unnest("tags", UnnestOptions.indexField("tagIndex")); + * .unnest("tags", "tag", Unnest.Options.DEFAULT.withIndexField("tagIndex")); * * // Output: - * // { "title": "The Hitchhiker's Guide to the Galaxy", "tagIndex": 0, "tags": "comedy", ... } - * // { "title": "The Hitchhiker's Guide to the Galaxy", "tagIndex": 1, "tags": "space", ... } - * // { "title": "The Hitchhiker's Guide to the Galaxy", "tagIndex": 2, "tags": "adventure", ... } + * // { "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. @@ -842,9 +877,9 @@ public Pipeline unnest(String fieldName) { * @return A new {@code Pipeline} object with this stage appended to the stage list. */ @BetaApi - public Pipeline unnest(String fieldName, UnnestOptions options) { + public Pipeline unnest(String fieldName, String alias, UnnestOptions options) { // return unnest(Field.of(fieldName), options); - return append(new Unnest(Field.of(fieldName), options)); + return append(new Unnest(Field.of(fieldName), alias, options)); } // /** @@ -905,12 +940,13 @@ public Pipeline unnest(String fieldName, UnnestOptions options) { * * @param name The unique name of the generic stage to add. * @param params A map of parameters to configure the generic stage's behavior. + * @param optionalParams Named optional parameters to configure the generic stage's behavior. * @return A new {@code Pipeline} object with this stage appended to the stage list. */ @BetaApi - public Pipeline genericStage(String name, List params) { + public Pipeline genericStage(String name, List params, GenericOptions optionalParams) { // Implementation for genericStage (add the GenericStage if needed) - return append(new GenericStage(name, params)); // Assuming GenericStage takes a list of params + return append(new GenericStage(name, params, optionalParams)); // Assuming GenericStage takes a list of params } /** @@ -946,7 +982,12 @@ public Pipeline genericStage(String name, List params) { */ @BetaApi public ApiFuture> execute() { - return execute((ByteString) null, (com.google.protobuf.Timestamp) null); + return execute(ExecuteOptions.DEFAULT, (ByteString) null, (com.google.protobuf.Timestamp) null); + } + + @BetaApi + public ApiFuture> execute(ExecuteOptions options) { + return execute(options, (ByteString) null, (com.google.protobuf.Timestamp) null); } /** @@ -996,7 +1037,7 @@ public ApiFuture> execute() { */ @BetaApi public void execute(ApiStreamObserver observer) { - executeInternal(null, null, observer); + executeInternal(ExecuteOptions.DEFAULT, null, null, observer); } // @BetaApi @@ -1016,10 +1057,13 @@ public void execute(ApiStreamObserver observer) { // } ApiFuture> execute( - @Nullable final ByteString transactionId, @Nullable com.google.protobuf.Timestamp readTime) { + @Nonnull ExecuteOptions options, + @Nullable final ByteString transactionId, + @Nullable com.google.protobuf.Timestamp readTime) { SettableApiFuture> futureResult = SettableApiFuture.create(); executeInternal( + options, transactionId, readTime, new PipelineResultObserver() { @@ -1045,13 +1089,17 @@ public void onError(Throwable t) { } void executeInternal( + @Nonnull ExecuteOptions options, @Nullable final ByteString transactionId, @Nullable com.google.protobuf.Timestamp readTime, ApiStreamObserver observer) { ExecutePipelineRequest.Builder request = ExecutePipelineRequest.newBuilder() .setDatabase(rpcContext.getDatabaseName()) - .setStructuredPipeline(StructuredPipeline.newBuilder().setPipeline(toProto()).build()); + .setStructuredPipeline(StructuredPipeline.newBuilder() + .setPipeline(toProto()) + .putAllOptions(StageUtils.toMap(options)) + .build()); if (transactionId != null) { request.setTransaction(transactionId); @@ -1164,18 +1212,18 @@ public void onComplete() { rpcContext.streamRequest(request, observer, rpcContext.getClient().executePipelineCallable()); } -} -@InternalExtensionOnly -abstract class PipelineResultObserver implements ApiStreamObserver { - private Timestamp executionTime; // Remove optional since Java doesn't have it + @InternalExtensionOnly + static abstract class PipelineResultObserver implements ApiStreamObserver { + private Timestamp executionTime; // Remove optional since Java doesn't have it - public void onCompleted(Timestamp executionTime) { - this.executionTime = executionTime; - this.onCompleted(); - } + public void onCompleted(Timestamp executionTime) { + this.executionTime = executionTime; + this.onCompleted(); + } - public Timestamp getExecutionTime() { // Add getter for executionTime - return executionTime; + public Timestamp getExecutionTime() { // Add getter for executionTime + return executionTime; + } } } 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 index d2fb06d30..c9f341a6f 100644 --- 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 @@ -20,6 +20,8 @@ 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; @@ -45,7 +47,7 @@ * } */ @BetaApi -public class PipelineSource { +public final class PipelineSource { private final FirestoreRpcContext rpcContext; @InternalApi @@ -62,7 +64,13 @@ public class PipelineSource { @Nonnull @BetaApi public Pipeline collection(@Nonnull String path) { - return new Pipeline(this.rpcContext, new Collection(path)); + return collection(path, CollectionOptions.DEFAULT); + } + + @Nonnull + @BetaApi + public Pipeline collection(@Nonnull String path, CollectionOptions options) { + return new Pipeline(this.rpcContext, new Collection(path, options)); } /** @@ -78,11 +86,17 @@ public Pipeline collection(@Nonnull String path) { @Nonnull @BetaApi public Pipeline collectionGroup(@Nonnull String collectionId) { + return collectionGroup(collectionId, CollectionGroupOptions.DEFAULT); + } + + @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)); + return new Pipeline(this.rpcContext, new CollectionGroup(collectionId, options)); } /** 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 index 2b12c6e90..6684b6e6a 100644 --- 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 @@ -22,6 +22,7 @@ import static com.google.cloud.firestore.pipeline.expressions.Function.inAny; import static com.google.cloud.firestore.pipeline.expressions.Function.not; import static com.google.cloud.firestore.pipeline.expressions.Function.or; +import static com.google.cloud.firestore.pipeline.expressions.FunctionUtils.exprToValue; import com.google.api.core.InternalApi; import com.google.cloud.firestore.Query.ComparisonFilterInternal; @@ -38,6 +39,7 @@ 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; @@ -51,6 +53,26 @@ public static Value encodeValue(Object value) { return UserDataConverter.encodeValue(FieldPath.empty(), value, UserDataConverter.ARGUMENT); } + @InternalApi + public static Value encodeValue(Expr value) { + return exprToValue(value); + } + + @InternalApi + public static Value encodeValue(String value) { + return Value.newBuilder().setStringValue(value).build(); + } + + @InternalApi + public static Value encodeValue(long value) { + return Value.newBuilder().setIntegerValue(value).build(); + } + + @InternalApi + public static Value encodeValue(Map options) { + return Value.newBuilder().setMapValue(MapValue.newBuilder().putAllFields(options).build()).build(); + } + @InternalApi static FilterCondition toPipelineFilterCondition(FilterInternal f) { if (f instanceof ComparisonFilterInternal) { 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 762ada8e1..079299fb9 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.ExecuteOptions; import com.google.cloud.firestore.telemetry.TraceUtil; import com.google.common.base.Preconditions; import com.google.common.util.concurrent.MoreExecutors; @@ -129,7 +130,7 @@ public ApiFuture get(@Nonnull AggregateQuery query) { @Override public ApiFuture> execute(@Nonnull Pipeline pipeline) { try (TraceUtil.Scope ignored = transactionTraceContext.makeCurrent()) { - return pipeline.execute(null, readTime); + return pipeline.execute(ExecuteOptions.DEFAULT, null, readTime); } } 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 ddbcc2bec..9b5f5d971 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.ExecuteOptions; import com.google.cloud.firestore.telemetry.TraceUtil; import com.google.common.base.Preconditions; import com.google.common.util.concurrent.MoreExecutors; @@ -265,7 +266,7 @@ public ApiFuture get(@Nonnull AggregateQuery query) { @Override public ApiFuture> execute(@Nonnull Pipeline pipeline) { try (TraceUtil.Scope ignored = transactionTraceContext.makeCurrent()) { - return pipeline.execute(transactionId, null); + return pipeline.execute(ExecuteOptions.DEFAULT, transactionId, null); } } } 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..b41a788ff --- /dev/null +++ b/google-cloud-firestore/src/main/java/com/google/cloud/firestore/pipeline/stages/AbstractOptions.java @@ -0,0 +1,66 @@ +/* + * 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.Expr; +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.List; + +abstract class AbstractOptions { + + protected final InternalOptions options; + + AbstractOptions(InternalOptions options) { + this.options = options; + } + + abstract T self(InternalOptions options); + + public T with(String key, String value) { + return self(options.with(key, encodeValue(value))); + } + + public T with(String key, long value) { + return self(options.with(key, encodeValue(value))); + } + + public T with(String key, Field value) { + return self(options.with(key, value.toProto())); + } + + protected T with(String key, List expressions) { + return self(options.with(key, Lists.transform(expressions, FunctionUtils::exprToValue))); + } + + protected T with(String key, AbstractOptions subSection) { + return self(options.with(key, subSection.options)); + } + + public T withSection(String key, GenericOptions subSection) { + return with(key, subSection); + } + + 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 index bec9411bd..b7963fbb5 100644 --- 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 @@ -20,22 +20,23 @@ import com.google.api.core.InternalApi; import com.google.cloud.firestore.pipeline.expressions.Expr; -import com.google.firestore.v1.Pipeline; +import com.google.firestore.v1.Value; +import java.util.Collections; import java.util.Map; @InternalApi public final class AddFields extends Stage { - private static final String name = "add_fields"; private final Map fields; @InternalApi public AddFields(Map fields) { + super("add_fields", InternalOptions.EMPTY); this.fields = fields; } @Override - Pipeline.Stage toStageProto() { - return Pipeline.Stage.newBuilder().setName(name).addArgs(encodeValue(fields)).build(); + 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 index 520c274b2..327c232da 100644 --- 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 @@ -24,53 +24,57 @@ import com.google.cloud.firestore.pipeline.expressions.Expr; import com.google.cloud.firestore.pipeline.expressions.ExprWithAlias; import com.google.cloud.firestore.pipeline.expressions.Selectable; -import com.google.firestore.v1.Pipeline; +import com.google.firestore.v1.Value; import java.util.Arrays; import java.util.Collections; import java.util.Map; import java.util.stream.Collectors; +import javax.annotation.Nonnull; @BetaApi public final class Aggregate extends Stage { - private static final String name = "aggregate"; private final Map groups; private final Map accumulators; @BetaApi public Aggregate withGroups(String... fields) { - return new Aggregate(PipelineUtils.fieldNamesToMap(fields), this.accumulators); + return new Aggregate(PipelineUtils.fieldNamesToMap(fields), this.accumulators, AggregateOptions.DEFAULT); } @BetaApi public Aggregate withGroups(Selectable... selectables) { - return new Aggregate(PipelineUtils.selectablesToMap(selectables), this.accumulators); + return new Aggregate(PipelineUtils.selectablesToMap(selectables), this.accumulators, AggregateOptions.DEFAULT); } @BetaApi public static Aggregate withAccumulators(ExprWithAlias... accumulators) { - if (accumulators.length == 0) { - throw new IllegalArgumentException( - "Must specify at least one accumulator for aggregate() stage. There is a distinct() stage if only distinct group values are needed."); - } - return new Aggregate( Collections.emptyMap(), Arrays.stream(accumulators) - .collect(Collectors.toMap(ExprWithAlias::getAlias, ExprWithAlias::getExpr))); + .collect(Collectors.toMap(ExprWithAlias::getAlias, ExprWithAlias::getExpr)), + AggregateOptions.DEFAULT); } - private Aggregate(Map groups, Map accumulators) { + @BetaApi + 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 - Pipeline.Stage toStageProto() { - return Pipeline.Stage.newBuilder() - .setName(name) - .addArgs(encodeValue(accumulators)) - .addArgs(encodeValue(groups)) - .build(); + Iterable toStageArgs() { + return Arrays.asList(encodeValue(accumulators), encodeValue(groups)); } } 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..64b7b067b --- /dev/null +++ b/google-cloud-firestore/src/main/java/com/google/cloud/firestore/pipeline/stages/AggregateOptions.java @@ -0,0 +1,31 @@ +/* + * 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 class AggregateOptions extends AbstractOptions { + + public static AggregateOptions DEFAULT = new AggregateOptions(InternalOptions.EMPTY); + + public AggregateOptions(InternalOptions options) { + super(options); + } + + @Override + AggregateOptions self(InternalOptions options) { + return new AggregateOptions(options); + } +} 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 index 1dbcfd028..0d049b0ee 100644 --- 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 @@ -17,18 +17,17 @@ package com.google.cloud.firestore.pipeline.stages; import com.google.api.core.InternalApi; -import com.google.firestore.v1.Pipeline; import com.google.firestore.v1.Value; +import java.util.Collections; import javax.annotation.Nonnull; @InternalApi public final class Collection extends Stage { - private static final String name = "collection"; @Nonnull private final String path; - @InternalApi - public Collection(@Nonnull String path) { + public Collection(@Nonnull String path, CollectionOptions options) { + super("collection", options.options); if (!path.startsWith("/")) { this.path = "/" + path; } else { @@ -36,11 +35,12 @@ public Collection(@Nonnull String path) { } } + public Collection withOptions(CollectionOptions options) { + return new Collection(path, options); + } + @Override - Pipeline.Stage toStageProto() { - return Pipeline.Stage.newBuilder() - .setName(name) - .addArgs(Value.newBuilder().setReferenceValue(path).build()) - .build(); + 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 index 186b06c09..5df5d4df9 100644 --- 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 @@ -19,26 +19,28 @@ import static com.google.cloud.firestore.PipelineUtils.encodeValue; import com.google.api.core.InternalApi; -import com.google.firestore.v1.Pipeline; +import com.google.common.collect.ImmutableList; import com.google.firestore.v1.Value; @InternalApi public final class CollectionGroup extends Stage { - private static final String name = "collection_group"; private final String collectionId; @InternalApi - public CollectionGroup(String collectionId) { + 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 - Pipeline.Stage toStageProto() { - return Pipeline.Stage.newBuilder() - .setName(name) - .addArgs(Value.newBuilder().setReferenceValue("").build()) - .addArgs(encodeValue(collectionId)) - .build(); + 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..1abb2c62a --- /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; + +import com.google.cloud.firestore.pipeline.stages.CollectionOptions.Hints; + +public class CollectionGroupOptions extends AbstractOptions { + + public static final CollectionGroupOptions DEFAULT = new CollectionGroupOptions(InternalOptions.EMPTY); + + CollectionGroupOptions(InternalOptions options) { + super(options); + } + + @Override + CollectionGroupOptions self(InternalOptions options) { + return new CollectionGroupOptions(options); + } + + public CollectionGroupOptions withHints(Hints hints) { + return with("hints", hints); + } +} 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..e1596b688 --- /dev/null +++ b/google-cloud-firestore/src/main/java/com/google/cloud/firestore/pipeline/stages/CollectionOptions.java @@ -0,0 +1,56 @@ +/* + * 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.expressions.Field; +import java.util.Arrays; + +public class CollectionOptions extends AbstractOptions { + + public static final CollectionOptions DEFAULT = new CollectionOptions(InternalOptions.EMPTY); + + CollectionOptions(InternalOptions options) { + super(options); + } + + @Override + CollectionOptions self(InternalOptions options) { + return new CollectionOptions(options); + } + + public CollectionOptions withHints(Hints hints) { + return with("hints", hints); + } + + public static class Hints extends AbstractOptions { + + public static Hints DEFAULT = new Hints(InternalOptions.EMPTY); + + Hints(InternalOptions options) { + super(options); + } + + @Override + Hints self(InternalOptions options) { + return new Hints(options); + } + + public Hints withIgnoreIndexFields(Field... fields) { + return with("ignore_index_fields", Arrays.asList(fields)); + } + } +} 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 index 8fd98ca03..121cde8f5 100644 --- 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 @@ -17,17 +17,19 @@ package com.google.cloud.firestore.pipeline.stages; import com.google.api.core.InternalApi; -import com.google.firestore.v1.Pipeline; +import com.google.firestore.v1.Value; +import java.util.Collections; @InternalApi public final class Database extends Stage { - private static final String name = "database"; @InternalApi - public Database() {} + public Database() { + super("database", InternalOptions.EMPTY); + } @Override - Pipeline.Stage toStageProto() { - return Pipeline.Stage.newBuilder().setName(name).build(); + 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 index eafbdce68..03e7b486a 100644 --- 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 @@ -21,22 +21,23 @@ import com.google.api.core.BetaApi; import com.google.api.core.InternalApi; import com.google.cloud.firestore.pipeline.expressions.Expr; -import com.google.firestore.v1.Pipeline; +import com.google.firestore.v1.Value; +import java.util.Collections; import java.util.Map; @BetaApi public final class Distinct extends Stage { - private static final String name = "distinct"; private final Map groups; @InternalApi public Distinct(Map groups) { + super("distinct", InternalOptions.EMPTY); this.groups = groups; } @Override - Pipeline.Stage toStageProto() { - return Pipeline.Stage.newBuilder().setName(name).addArgs(encodeValue(groups)).build(); + 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 index f20d79898..5a26a35d9 100644 --- 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 @@ -18,7 +18,9 @@ import com.google.api.core.InternalApi; import com.google.cloud.firestore.DocumentReference; -import com.google.firestore.v1.Pipeline; +import com.google.cloud.firestore.PipelineUtils; +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; @@ -26,11 +28,11 @@ @InternalApi public final class Documents extends Stage { - private static final String name = "documents"; private List documents; @InternalApi Documents(List documents) { + super("documents", InternalOptions.EMPTY); this.documents = documents; } @@ -41,11 +43,7 @@ public static Documents of(DocumentReference... documents) { } @Override - Pipeline.Stage toStageProto() { - Pipeline.Stage.Builder builder = Pipeline.Stage.newBuilder().setName(name); - for (String document : documents) { - builder.addArgsBuilder().setStringValue(document); - } - return builder.build(); + Iterable toStageArgs() { + return Iterables.transform(documents, PipelineUtils::encodeValue); } } diff --git a/google-cloud-firestore/src/main/java/com/google/cloud/firestore/pipeline/stages/ExecuteOptions.java b/google-cloud-firestore/src/main/java/com/google/cloud/firestore/pipeline/stages/ExecuteOptions.java new file mode 100644 index 000000000..e0af34923 --- /dev/null +++ b/google-cloud-firestore/src/main/java/com/google/cloud/firestore/pipeline/stages/ExecuteOptions.java @@ -0,0 +1,31 @@ +/* + * 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 class ExecuteOptions extends AbstractOptions { + + public static ExecuteOptions DEFAULT = new ExecuteOptions(InternalOptions.EMPTY); + + ExecuteOptions(InternalOptions options) { + super(options); + } + + @Override + ExecuteOptions self(InternalOptions options) { + return new ExecuteOptions(options); + } +} 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 index 2b41de29b..9cc75339b 100644 --- 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 @@ -21,105 +21,47 @@ import com.google.api.core.BetaApi; import com.google.api.core.InternalApi; import com.google.cloud.firestore.pipeline.expressions.Expr; -import com.google.firestore.v1.Pipeline; +import com.google.common.collect.ImmutableList; +import com.google.firestore.v1.Value; @BetaApi public final class FindNearest extends Stage { - public interface DistanceMeasure { + public final static class DistanceMeasure { - enum Type { - EUCLIDEAN, - COSINE, - DOT_PRODUCT - } - - static DistanceMeasure euclidean() { - return new EuclideanDistanceMeasure(); - } - - static DistanceMeasure cosine() { - return new CosineDistanceMeasure(); - } - - static DistanceMeasure dotProduct() { - return new DotProductDistanceMeasure(); - } - - static DistanceMeasure generic(String name) { - return new GenericDistanceMeasure(name); - } - - @InternalApi - String toProtoString(); - } - - public static class EuclideanDistanceMeasure implements DistanceMeasure { + final String protoString; - @Override - @InternalApi - public String toProtoString() { - return "euclidean"; + private DistanceMeasure(String protoString) { + this.protoString = protoString; } - } - - public static class CosineDistanceMeasure implements DistanceMeasure { - - @Override - @InternalApi - public String toProtoString() { - return "cosine"; - } - } - - public static class DotProductDistanceMeasure implements DistanceMeasure { - - @Override - @InternalApi - public String toProtoString() { - return "dot_product"; - } - } - - public static class GenericDistanceMeasure implements DistanceMeasure { - - String name; - public GenericDistanceMeasure(String name) { - this.name = name; + 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); } - @Override - @InternalApi - public String toProtoString() { - return name; + Value toProto() { + return encodeValue(protoString); } } - private static final String name = "find_nearest"; private final Expr property; private final double[] vector; private final DistanceMeasure distanceMeasure; - private final FindNearestOptions options; @InternalApi public FindNearest( Expr property, double[] vector, DistanceMeasure distanceMeasure, FindNearestOptions options) { + super("find_nearest", options.options); this.property = property; this.vector = vector; this.distanceMeasure = distanceMeasure; - this.options = options; } @Override - Pipeline.Stage toStageProto() { - return Pipeline.Stage.newBuilder() - .setName(name) - .addArgs(encodeValue(property)) - .addArgs(encodeValue(vector)) - .addArgs(encodeValue(distanceMeasure.toProtoString())) - .putOptions("limit", encodeValue(options.getLimit())) - .putOptions("distance_field", encodeValue(options.getDistanceField())) - .build(); + 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 index 1c57ac76e..1ddbea581 100644 --- 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 @@ -18,50 +18,26 @@ import com.google.api.core.BetaApi; import com.google.cloud.firestore.pipeline.expressions.Field; -import javax.annotation.Nullable; @BetaApi -public class FindNearestOptions { +public class FindNearestOptions extends AbstractOptions { - @Nullable private final Long limit; + public static FindNearestOptions DEFAULT = new FindNearestOptions(InternalOptions.EMPTY); - @Nullable private final Field distanceField; - - private FindNearestOptions(Long limit, Field distanceField) { - this.limit = limit; - this.distanceField = distanceField; - } - - public static Builder builder() { - return new Builder(); + private FindNearestOptions(InternalOptions options) { + super(options); } - @Nullable - public Long getLimit() { - return limit; + @Override + FindNearestOptions self(InternalOptions options) { + return new FindNearestOptions(options); } - @Nullable - public Field getDistanceField() { - return distanceField; + public FindNearestOptions withLimit(long limit) { + return with("limit", limit); } - public static class Builder { - @Nullable private Long limit; - @Nullable private Field distanceField; - - public Builder limit(Long limit) { - this.limit = limit; - return this; - } - - public Builder distanceField(Field distanceField) { - this.distanceField = distanceField; - return this; - } - - public FindNearestOptions build() { - return new FindNearestOptions(limit, distanceField); - } + public FindNearestOptions withDistanceField(Field distanceField) { + return with("distance_field", distanceField); } } diff --git a/google-cloud-firestore/src/main/java/com/google/cloud/firestore/pipeline/stages/GenericOptions.java b/google-cloud-firestore/src/main/java/com/google/cloud/firestore/pipeline/stages/GenericOptions.java new file mode 100644 index 000000000..a82f2c7aa --- /dev/null +++ b/google-cloud-firestore/src/main/java/com/google/cloud/firestore/pipeline/stages/GenericOptions.java @@ -0,0 +1,47 @@ +/* + * 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 GenericOptions extends AbstractOptions { + + public static GenericOptions DEFAULT = new GenericOptions(InternalOptions.EMPTY); + + public static GenericOptions of(String key, String value) { + return new GenericOptions(InternalOptions.of(key, encodeValue(value))); + } + + public static GenericOptions of(String key, long value) { + return new GenericOptions(InternalOptions.of(key, encodeValue(value))); + } + + public static GenericOptions of(String key, Field value) { + return new GenericOptions(InternalOptions.of(key, value.toProto())); + } + + GenericOptions(InternalOptions options) { + super(options); + } + + @Override + protected GenericOptions self(InternalOptions options) { + return new GenericOptions(options); + } +} diff --git a/google-cloud-firestore/src/main/java/com/google/cloud/firestore/pipeline/stages/GenericStage.java b/google-cloud-firestore/src/main/java/com/google/cloud/firestore/pipeline/stages/GenericStage.java index d2595f12c..dc0adf00a 100644 --- a/google-cloud-firestore/src/main/java/com/google/cloud/firestore/pipeline/stages/GenericStage.java +++ b/google-cloud-firestore/src/main/java/com/google/cloud/firestore/pipeline/stages/GenericStage.java @@ -19,27 +19,24 @@ import static com.google.cloud.firestore.PipelineUtils.encodeValue; import com.google.api.core.InternalApi; -import com.google.firestore.v1.Pipeline; +import com.google.cloud.firestore.PipelineUtils; +import com.google.common.collect.Iterables; +import com.google.firestore.v1.Value; import java.util.List; @InternalApi public final class GenericStage extends Stage { - private final String name; private List params; @InternalApi - public GenericStage(String name, List params) { - this.name = name; + public GenericStage(String name, List params, GenericOptions optionalParams) { + super(name, optionalParams.options); this.params = params; } @Override - Pipeline.Stage toStageProto() { - Pipeline.Stage.Builder builder = Pipeline.Stage.newBuilder().setName(name); - for (Object param : params) { - builder.addArgs(encodeValue(param)); - } - return builder.build(); + Iterable toStageArgs() { + return Iterables.transform(params, PipelineUtils::encodeValue); } } 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..618516e6a --- /dev/null +++ b/google-cloud-firestore/src/main/java/com/google/cloud/firestore/pipeline/stages/InternalOptions.java @@ -0,0 +1,59 @@ +/* + * 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; + +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()); + } + + 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 index 5d73400ff..8723b980c 100644 --- 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 @@ -19,21 +19,22 @@ import static com.google.cloud.firestore.PipelineUtils.encodeValue; import com.google.api.core.InternalApi; -import com.google.firestore.v1.Pipeline; +import com.google.firestore.v1.Value; +import java.util.Collections; @InternalApi public final class Limit extends Stage { - private static final String name = "limit"; private final int limit; @InternalApi public Limit(int limit) { + super("limit", InternalOptions.EMPTY); this.limit = limit; } @Override - Pipeline.Stage toStageProto() { - return Pipeline.Stage.newBuilder().setName(name).addArgs(encodeValue(limit)).build(); + 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 index 49bb0aed4..63a96812a 100644 --- 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 @@ -19,21 +19,22 @@ import static com.google.cloud.firestore.PipelineUtils.encodeValue; import com.google.api.core.InternalApi; -import com.google.firestore.v1.Pipeline; +import com.google.firestore.v1.Value; +import java.util.Collections; @InternalApi public final class Offset extends Stage { - private static final String name = "offset"; private final int offset; @InternalApi public Offset(int offset) { + super("offset", InternalOptions.EMPTY); this.offset = offset; } @Override - Pipeline.Stage toStageProto() { - return Pipeline.Stage.newBuilder().setName(name).addArgs(encodeValue(offset)).build(); + Iterable toStageArgs() { + return Collections.singletonList(encodeValue(offset)); } } 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 index 794f28a8f..613f1bf0e 100644 --- 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 @@ -20,25 +20,22 @@ import com.google.cloud.firestore.PipelineUtils; import com.google.cloud.firestore.pipeline.expressions.Field; import com.google.common.collect.ImmutableList; -import com.google.firestore.v1.Pipeline; +import com.google.common.collect.Iterables; +import com.google.firestore.v1.Value; @InternalApi public final class RemoveFields extends Stage { - private static final String name = "remove_fields"; private final ImmutableList fields; @InternalApi public RemoveFields(ImmutableList fields) { + super("remove_fields", InternalOptions.EMPTY); this.fields = fields; } @Override - Pipeline.Stage toStageProto() { - Pipeline.Stage.Builder builder = Pipeline.Stage.newBuilder().setName(name); - for (Field field : fields) { - builder.addArgs(PipelineUtils.encodeValue(field)); - } - return builder.build(); + Iterable toStageArgs() { + return Iterables.transform(fields, PipelineUtils::encodeValue); } } diff --git a/google-cloud-firestore/src/main/java/com/google/cloud/firestore/pipeline/stages/Replace.java b/google-cloud-firestore/src/main/java/com/google/cloud/firestore/pipeline/stages/Replace.java index 6be322564..d4f99f368 100644 --- a/google-cloud-firestore/src/main/java/com/google/cloud/firestore/pipeline/stages/Replace.java +++ b/google-cloud-firestore/src/main/java/com/google/cloud/firestore/pipeline/stages/Replace.java @@ -19,13 +19,12 @@ import static com.google.cloud.firestore.PipelineUtils.encodeValue; import com.google.cloud.firestore.pipeline.expressions.Selectable; -import com.google.firestore.v1.Pipeline; +import com.google.common.collect.ImmutableList; import com.google.firestore.v1.Value; import javax.annotation.Nonnull; public class Replace extends Stage { - private static final String name = "replace"; private final Selectable field; private final Mode mode; @@ -46,16 +45,13 @@ public Replace(@Nonnull Selectable field) { } public Replace(@Nonnull Selectable field, @Nonnull Mode mode) { + super("replace", InternalOptions.EMPTY); this.field = field; this.mode = mode; } @Override - Pipeline.Stage toStageProto() { - return Pipeline.Stage.newBuilder() - .setName(name) - .addArgs(encodeValue(field)) - .addArgs(mode.value) - .build(); + Iterable toStageArgs() { + return ImmutableList.of(encodeValue(field), 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 index f30597a21..9115fe203 100644 --- 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 @@ -16,21 +16,54 @@ 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.firestore.v1.Pipeline; +import com.google.common.collect.ImmutableList; +import com.google.firestore.v1.Value; +import javax.annotation.Nonnull; public final class Sample extends Stage { - private static final String name = "sample"; - private final SampleOptions options; + 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, SampleOptions.DEFAULT); + } + + @BetaApi + public static Sample withDocLimit(int documents) { + return new Sample(documents, Mode.DOCUMENTS, SampleOptions.DEFAULT); + } + + @BetaApi + public Sample withOptions(@Nonnull SampleOptions options) { + return new Sample(size, mode, options); + } @InternalApi - public Sample(SampleOptions options) { - this.options = options; + private Sample(Number size, Mode mode, SampleOptions options) { + super("sample", options.options); + this.size = size; + this.mode = mode; } @Override - Pipeline.Stage toStageProto() { - return Pipeline.Stage.newBuilder().setName(name).addAllArgs(options.getProtoArgs()).build(); + Iterable toStageArgs() { + return ImmutableList.of(encodeValue(size), mode.value); } } 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 index 74763f9b0..311608a23 100644 --- 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 @@ -20,22 +20,23 @@ import com.google.api.core.InternalApi; import com.google.cloud.firestore.pipeline.expressions.Expr; -import com.google.firestore.v1.Pipeline; +import com.google.firestore.v1.Value; +import java.util.Collections; import java.util.Map; @InternalApi public final class Select extends Stage { - private static final String name = "select"; private final Map projections; @InternalApi public Select(Map projections) { + super("select", InternalOptions.EMPTY); this.projections = projections; } @Override - Pipeline.Stage toStageProto() { - return Pipeline.Stage.newBuilder().setName(name).addArgs(encodeValue(projections)).build(); + 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 index 70d92750f..c89266118 100644 --- 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 @@ -18,8 +18,9 @@ import com.google.api.core.InternalApi; import com.google.cloud.firestore.pipeline.expressions.Ordering; -import com.google.common.collect.Lists; -import com.google.firestore.v1.Pipeline; +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 { @@ -28,16 +29,13 @@ public final class Sort extends Stage { private final List orders; @InternalApi - public Sort(Ordering... orders) { - this.orders = Lists.newArrayList(orders); + public Sort(ImmutableList orders) { + super("sort", InternalOptions.EMPTY); + this.orders = orders; } @Override - Pipeline.Stage toStageProto() { - Pipeline.Stage.Builder builder = Pipeline.Stage.newBuilder().setName(name); - for (Ordering order : orders) { - builder.addArgs(order.toProto()); - } - return builder.build(); + 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 index a148d126b..8f82195f7 100644 --- 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 @@ -16,11 +16,28 @@ 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() {} + 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 com.google.firestore.v1.Pipeline.Stage toStageProto(); + 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 index d538e3dd1..bcb7e5ee0 100644 --- 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 @@ -17,6 +17,8 @@ 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 { @@ -24,4 +26,10 @@ public final class StageUtils { public static com.google.firestore.v1.Pipeline.Stage toStageProto(Stage stage) { return stage.toStageProto(); } + + @SuppressWarnings("ClassEscapesDefinedScope") + @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 index 4014e926a..3365e042f 100644 --- 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 @@ -17,20 +17,20 @@ package com.google.cloud.firestore.pipeline.stages; import com.google.cloud.firestore.Pipeline; +import com.google.firestore.v1.Value; +import java.util.Collections; public class Union extends Stage { - private static final String name = "union"; private final Pipeline other; public Union(Pipeline other) { + super("union", InternalOptions.EMPTY); this.other = other; } @Override - com.google.firestore.v1.Pipeline.Stage toStageProto() { - com.google.firestore.v1.Pipeline.Stage.Builder builder = - com.google.firestore.v1.Pipeline.Stage.newBuilder().setName(name); - return builder.addArgs(other.toProtoValue()).build(); + 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 index aaddc12b2..caf83793c 100644 --- 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 @@ -19,32 +19,30 @@ import static com.google.cloud.firestore.PipelineUtils.encodeValue; import com.google.cloud.firestore.pipeline.expressions.Field; -import com.google.firestore.v1.Pipeline; +import com.google.common.collect.ImmutableList; +import com.google.firestore.v1.Value; import javax.annotation.Nonnull; public class Unnest extends Stage { - private static final String name = "unnest"; private final Field field; - private final UnnestOptions options; + private final String alias; - public Unnest(Field field) { + public Unnest(@Nonnull Field field, @Nonnull String alias) { + super("unnest", InternalOptions.EMPTY); this.field = field; - this.options = null; + this.alias = alias; } - public Unnest(@Nonnull Field field, @Nonnull UnnestOptions options) { + public Unnest(@Nonnull Field field, @Nonnull String alias, @Nonnull UnnestOptions options) { + super("unnest", options.options); this.field = field; - this.options = options; + this.alias = alias; } @Override - Pipeline.Stage toStageProto() { - Pipeline.Stage.Builder builder = - Pipeline.Stage.newBuilder().setName(name).addArgs(encodeValue(field)); - if (options != null) { - builder.addArgs(encodeValue(options.indexField)); - } - return builder.build(); + Iterable toStageArgs() { + return ImmutableList.of(encodeValue(field), 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 index 3ca12d792..e744875ce 100644 --- 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 @@ -18,15 +18,20 @@ import javax.annotation.Nonnull; -public class UnnestOptions { +public class UnnestOptions extends AbstractOptions { - final String indexField; + public static UnnestOptions DEFAULT = new UnnestOptions(InternalOptions.EMPTY); - public static UnnestOptions indexField(@Nonnull String indexField) { - return new UnnestOptions(indexField); + public UnnestOptions withIndexField(@Nonnull String indexField) { + return with("index_field", indexField); } - private UnnestOptions(String indexField) { - this.indexField = indexField; + @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 index 36da8f12e..02511e4fa 100644 --- 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 @@ -20,21 +20,22 @@ import com.google.api.core.InternalApi; import com.google.cloud.firestore.pipeline.expressions.FilterCondition; -import com.google.firestore.v1.Pipeline; +import com.google.firestore.v1.Value; +import java.util.Collections; @InternalApi public final class Where extends Stage { - private static final String name = "where"; private final FilterCondition condition; @InternalApi public Where(FilterCondition condition) { + super("where", InternalOptions.EMPTY); this.condition = condition; } @Override - Pipeline.Stage toStageProto() { - return Pipeline.Stage.newBuilder().setName(name).addArgs(encodeValue(condition)).build(); + Iterable toStageArgs() { + return Collections.singletonList(encodeValue(condition)); } } 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 index 550d04fa1..c610110ba 100644 --- 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 @@ -49,7 +49,8 @@ import com.google.cloud.firestore.pipeline.expressions.Field; import com.google.cloud.firestore.pipeline.expressions.Function; import com.google.cloud.firestore.pipeline.stages.Aggregate; -import com.google.cloud.firestore.pipeline.stages.SampleOptions; +import com.google.cloud.firestore.pipeline.stages.AggregateOptions; +import com.google.cloud.firestore.pipeline.stages.Sample; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; import com.google.common.collect.Lists; @@ -261,7 +262,7 @@ public void testAggregates() throws Exception { firestore .pipeline() .collection(collection.getPath()) - .aggregate(countAll().as("count")) + .aggregate(AggregateOptions.DEFAULT, countAll().as("count")) .execute() .get(); assertThat(data(results)).isEqualTo(Lists.newArrayList(map("count", 10L))); @@ -1137,7 +1138,7 @@ public void testSampleLimit() throws Exception { @Test public void testSamplePercentage() throws Exception { List results = - collection.pipeline().sample(SampleOptions.percentage(0.6)).execute().get(); + collection.pipeline().sample(Sample.withPercentage(0.6)).execute().get(); assertThat(results).hasSize(6); } @@ -1156,7 +1157,7 @@ public void testUnnest() throws Exception { collection .pipeline() .where(eq(Field.of("title"), "The Hitchhiker's Guide to the Galaxy")) - .unnest("tags") + .unnest("tags", "tag") .execute() .get(); From 8701dd6bf7d32894f42ff9058bbf2a0241f71b49 Mon Sep 17 00:00:00 2001 From: Tom Andersen Date: Mon, 16 Dec 2024 11:10:03 -0500 Subject: [PATCH 2/7] Incomplete prototyping of pipeline options --- .../pipeline/stages/SampleOptions.java | 39 ++++--------------- 1 file changed, 7 insertions(+), 32 deletions(-) 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 index fe7524dd6..75d39f8e7 100644 --- 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 @@ -16,41 +16,16 @@ package com.google.cloud.firestore.pipeline.stages; -import static com.google.cloud.firestore.PipelineUtils.encodeValue; +public class SampleOptions extends AbstractOptions { -import com.google.common.collect.ImmutableList; -import com.google.firestore.v1.Value; + public static SampleOptions DEFAULT = new SampleOptions(InternalOptions.EMPTY); -public class SampleOptions { - - private final Number n; - private final Mode mode; - - private SampleOptions(Number n, Mode mode) { - this.n = n; - this.mode = mode; - } - - public enum Mode { - DOCUMENTS(Value.newBuilder().setStringValue("documents").build()), - PERCENT(Value.newBuilder().setStringValue("percent").build()); - - public final Value value; - - Mode(Value value) { - this.value = value; - } - } - - public static SampleOptions percentage(double percentage) { - return new SampleOptions(percentage, Mode.PERCENT); - } - - public static SampleOptions docLimit(int documents) { - return new SampleOptions(documents, Mode.DOCUMENTS); + public SampleOptions(InternalOptions options) { + super(options); } - Iterable getProtoArgs() { - return ImmutableList.of(encodeValue(n), mode.value); + @Override + SampleOptions self(InternalOptions options) { + return new SampleOptions(options); } } From 521f66108ba11319ebfd9b776f7b0fe5292169e1 Mon Sep 17 00:00:00 2001 From: Tom Andersen Date: Mon, 16 Dec 2024 11:36:32 -0500 Subject: [PATCH 3/7] Incomplete prototyping of pipeline options --- .../com/google/cloud/firestore/PipelineUtils.java | 12 ++++++++++++ .../google/cloud/firestore/UserDataConverter.java | 6 +++++- .../firestore/pipeline/stages/AbstractOptions.java | 9 +++++++++ .../firestore/pipeline/stages/GenericOptions.java | 9 +++++++++ 4 files changed, 35 insertions(+), 1 deletion(-) 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 index 6684b6e6a..64bf774a8 100644 --- 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 @@ -25,6 +25,7 @@ import static com.google.cloud.firestore.pipeline.expressions.FunctionUtils.exprToValue; import com.google.api.core.InternalApi; +import com.google.cloud.Timestamp; import com.google.cloud.firestore.Query.ComparisonFilterInternal; import com.google.cloud.firestore.Query.CompositeFilterInternal; import com.google.cloud.firestore.Query.FilterInternal; @@ -41,6 +42,7 @@ import com.google.firestore.v1.Cursor; import com.google.firestore.v1.MapValue; import com.google.firestore.v1.Value; +import com.google.protobuf.NullValue; import java.util.HashMap; import java.util.List; import java.util.Map; @@ -63,11 +65,21 @@ 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(); 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 e73a45124..f4f81a1f8 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 @@ -41,8 +41,12 @@ /** Converts user input into the Firestore Value representation. */ class UserDataConverter { + private static final Logger LOGGER = Logger.getLogger(UserDataConverter.class.getName()); + private static final Value VALUE_OF_NULL = Value.newBuilder().setNullValue(NullValue.NULL_VALUE) + .build(); + /** Controls the behavior for field deletes. */ interface EncodingOptions { /** Returns whether a field delete at `fieldPath` is allowed. */ @@ -121,7 +125,7 @@ static Value encodeValue( path); return null; } else if (sanitizedObject == null) { - return Value.newBuilder().setNullValue(NullValue.NULL_VALUE).build(); + return VALUE_OF_NULL; } else if (sanitizedObject instanceof String) { return Value.newBuilder().setStringValue((String) sanitizedObject).build(); } else if (sanitizedObject instanceof Integer) { 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 index b41a788ff..98de84490 100644 --- 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 @@ -18,6 +18,7 @@ import static com.google.cloud.firestore.PipelineUtils.encodeValue; +import com.google.cloud.Timestamp; import com.google.cloud.firestore.pipeline.expressions.Expr; import com.google.cloud.firestore.pipeline.expressions.Field; import com.google.cloud.firestore.pipeline.expressions.FunctionUtils; @@ -40,10 +41,18 @@ public T with(String key, String value) { return self(options.with(key, encodeValue(value))); } + public T with(String key, boolean value) { + return self(options.with(key, encodeValue(value))); + } + public T with(String key, long value) { return self(options.with(key, encodeValue(value))); } + public T with(String key, double value) { + return self(options.with(key, encodeValue(value))); + } + public T with(String key, Field value) { return self(options.with(key, value.toProto())); } diff --git a/google-cloud-firestore/src/main/java/com/google/cloud/firestore/pipeline/stages/GenericOptions.java b/google-cloud-firestore/src/main/java/com/google/cloud/firestore/pipeline/stages/GenericOptions.java index a82f2c7aa..67a7795ea 100644 --- a/google-cloud-firestore/src/main/java/com/google/cloud/firestore/pipeline/stages/GenericOptions.java +++ b/google-cloud-firestore/src/main/java/com/google/cloud/firestore/pipeline/stages/GenericOptions.java @@ -18,6 +18,7 @@ import static com.google.cloud.firestore.PipelineUtils.encodeValue; +import com.google.cloud.Timestamp; import com.google.cloud.firestore.pipeline.expressions.Field; public final class GenericOptions extends AbstractOptions { @@ -28,10 +29,18 @@ public static GenericOptions of(String key, String value) { return new GenericOptions(InternalOptions.of(key, encodeValue(value))); } + public static GenericOptions of(String key, boolean value) { + return new GenericOptions(InternalOptions.of(key, encodeValue(value))); + } + public static GenericOptions of(String key, long value) { return new GenericOptions(InternalOptions.of(key, encodeValue(value))); } + public static GenericOptions of(String key, double value) { + return new GenericOptions(InternalOptions.of(key, encodeValue(value))); + } + public static GenericOptions of(String key, Field value) { return new GenericOptions(InternalOptions.of(key, value.toProto())); } From 814906ab687da636ab54b9326da78327cb49ffb0 Mon Sep 17 00:00:00 2001 From: Tom Andersen Date: Mon, 16 Dec 2024 13:28:45 -0500 Subject: [PATCH 4/7] Incomplete prototyping of pipeline options --- .../com/google/cloud/firestore/Pipeline.java | 16 ++---- .../cloud/firestore/PipelineSource.java | 7 +++ .../google/cloud/firestore/PipelineUtils.java | 2 - .../cloud/firestore/ReadTimeTransaction.java | 4 +- .../firestore/ServerSideTransaction.java | 4 +- .../pipeline/stages/AbstractOptions.java | 39 ++++++++----- ...xecuteOptions.java => AggregateHints.java} | 14 +++-- .../pipeline/stages/AggregateOptions.java | 6 +- .../stages/CollectionGroupOptions.java | 6 +- .../pipeline/stages/CollectionHints.java | 39 +++++++++++++ .../pipeline/stages/CollectionOptions.java | 24 +------- .../pipeline/stages/FindNearestOptions.java | 2 +- .../pipeline/stages/GenericOptions.java | 1 - .../pipeline/stages/PipelineOptions.java | 56 +++++++++++++++++++ .../pipeline/stages/SampleOptions.java | 2 +- .../firestore/pipeline/stages/Union.java | 2 +- .../firestore/pipeline/stages/Unnest.java | 2 +- .../pipeline/stages/UnnestOptions.java | 2 +- .../cloud/firestore/it/ITPipelineTest.java | 31 ++++++++++ 19 files changed, 190 insertions(+), 69 deletions(-) rename google-cloud-firestore/src/main/java/com/google/cloud/firestore/pipeline/stages/{ExecuteOptions.java => AggregateHints.java} (65%) create mode 100644 google-cloud-firestore/src/main/java/com/google/cloud/firestore/pipeline/stages/CollectionHints.java create mode 100644 google-cloud-firestore/src/main/java/com/google/cloud/firestore/pipeline/stages/PipelineOptions.java 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 index e10dafbc8..c7522b265 100644 --- 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 @@ -26,10 +26,7 @@ import com.google.api.gax.rpc.StreamController; import com.google.cloud.Timestamp; import com.google.cloud.firestore.pipeline.stages.AggregateOptions; -import com.google.cloud.firestore.pipeline.stages.Collection; -import com.google.cloud.firestore.pipeline.stages.CollectionOptions; -import com.google.cloud.firestore.pipeline.stages.ExecuteOptions; -import com.google.cloud.firestore.pipeline.stages.FindNearest.DistanceMeasure; +import com.google.cloud.firestore.pipeline.stages.PipelineOptions; import com.google.cloud.firestore.pipeline.stages.GenericOptions; import com.google.cloud.firestore.pipeline.expressions.Accumulator; import com.google.cloud.firestore.pipeline.expressions.Expr; @@ -50,7 +47,6 @@ import com.google.cloud.firestore.pipeline.stages.RemoveFields; import com.google.cloud.firestore.pipeline.stages.Replace; import com.google.cloud.firestore.pipeline.stages.Sample; -import com.google.cloud.firestore.pipeline.stages.SampleOptions; import com.google.cloud.firestore.pipeline.stages.Select; import com.google.cloud.firestore.pipeline.stages.Sort; import com.google.cloud.firestore.pipeline.stages.Stage; @@ -982,11 +978,11 @@ public Pipeline genericStage(String name, List params, GenericOptions op */ @BetaApi public ApiFuture> execute() { - return execute(ExecuteOptions.DEFAULT, (ByteString) null, (com.google.protobuf.Timestamp) null); + return execute(PipelineOptions.DEFAULT, (ByteString) null, (com.google.protobuf.Timestamp) null); } @BetaApi - public ApiFuture> execute(ExecuteOptions options) { + public ApiFuture> execute(PipelineOptions options) { return execute(options, (ByteString) null, (com.google.protobuf.Timestamp) null); } @@ -1037,7 +1033,7 @@ public ApiFuture> execute(ExecuteOptions options) { */ @BetaApi public void execute(ApiStreamObserver observer) { - executeInternal(ExecuteOptions.DEFAULT, null, null, observer); + executeInternal(PipelineOptions.DEFAULT, null, null, observer); } // @BetaApi @@ -1057,7 +1053,7 @@ public void execute(ApiStreamObserver observer) { // } ApiFuture> execute( - @Nonnull ExecuteOptions options, + @Nonnull PipelineOptions options, @Nullable final ByteString transactionId, @Nullable com.google.protobuf.Timestamp readTime) { SettableApiFuture> futureResult = SettableApiFuture.create(); @@ -1089,7 +1085,7 @@ public void onError(Throwable t) { } void executeInternal( - @Nonnull ExecuteOptions options, + @Nonnull PipelineOptions options, @Nullable final ByteString transactionId, @Nullable com.google.protobuf.Timestamp readTime, ApiStreamObserver observer) { 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 index c9f341a6f..14fc0f852 100644 --- 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 @@ -21,6 +21,7 @@ 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.CollectionHints; import com.google.cloud.firestore.pipeline.stages.CollectionOptions; import com.google.cloud.firestore.pipeline.stages.Database; import com.google.cloud.firestore.pipeline.stages.Documents; @@ -73,6 +74,12 @@ public Pipeline collection(@Nonnull String path, CollectionOptions options) { return new Pipeline(this.rpcContext, new Collection(path, options)); } + @Nonnull + @BetaApi + public Pipeline collection(@Nonnull String path, CollectionHints hints) { + return new Pipeline(this.rpcContext, new Collection(path, CollectionOptions.DEFAULT.withHints(hints))); + } + /** * Creates a new {@link Pipeline} that operates on all documents in a collection group. * 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 index 64bf774a8..fa88e79f0 100644 --- 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 @@ -25,7 +25,6 @@ import static com.google.cloud.firestore.pipeline.expressions.FunctionUtils.exprToValue; import com.google.api.core.InternalApi; -import com.google.cloud.Timestamp; import com.google.cloud.firestore.Query.ComparisonFilterInternal; import com.google.cloud.firestore.Query.CompositeFilterInternal; import com.google.cloud.firestore.Query.FilterInternal; @@ -42,7 +41,6 @@ import com.google.firestore.v1.Cursor; import com.google.firestore.v1.MapValue; import com.google.firestore.v1.Value; -import com.google.protobuf.NullValue; import java.util.HashMap; import java.util.List; import java.util.Map; 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 079299fb9..093625a4d 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,7 +18,7 @@ import com.google.api.core.ApiFuture; import com.google.api.core.ApiFutures; -import com.google.cloud.firestore.pipeline.stages.ExecuteOptions; +import com.google.cloud.firestore.pipeline.stages.PipelineOptions; import com.google.cloud.firestore.telemetry.TraceUtil; import com.google.common.base.Preconditions; import com.google.common.util.concurrent.MoreExecutors; @@ -130,7 +130,7 @@ public ApiFuture get(@Nonnull AggregateQuery query) { @Override public ApiFuture> execute(@Nonnull Pipeline pipeline) { try (TraceUtil.Scope ignored = transactionTraceContext.makeCurrent()) { - return pipeline.execute(ExecuteOptions.DEFAULT, null, readTime); + return pipeline.execute(PipelineOptions.DEFAULT, null, readTime); } } 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 9b5f5d971..df1828210 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,7 +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.ExecuteOptions; +import com.google.cloud.firestore.pipeline.stages.PipelineOptions; import com.google.cloud.firestore.telemetry.TraceUtil; import com.google.common.base.Preconditions; import com.google.common.util.concurrent.MoreExecutors; @@ -266,7 +266,7 @@ public ApiFuture get(@Nonnull AggregateQuery query) { @Override public ApiFuture> execute(@Nonnull Pipeline pipeline) { try (TraceUtil.Scope ignored = transactionTraceContext.makeCurrent()) { - return pipeline.execute(ExecuteOptions.DEFAULT, transactionId, null); + return pipeline.execute(PipelineOptions.DEFAULT, transactionId, null); } } } 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 index 98de84490..8e6364d52 100644 --- 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 @@ -18,13 +18,14 @@ import static com.google.cloud.firestore.PipelineUtils.encodeValue; -import com.google.cloud.Timestamp; +import com.google.cloud.firestore.PipelineUtils; import com.google.cloud.firestore.pipeline.expressions.Expr; 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; abstract class AbstractOptions { @@ -37,39 +38,47 @@ abstract class AbstractOptions { abstract T self(InternalOptions options); - public T with(String key, String value) { - return self(options.with(key, encodeValue(value))); + public final T with(String key, String value) { + return with(key, encodeValue(value)); } - public T with(String key, boolean value) { - return self(options.with(key, encodeValue(value))); + public final T with(String key, boolean value) { + return with(key, encodeValue(value)); } - public T with(String key, long value) { - return self(options.with(key, encodeValue(value))); + public final T with(String key, long value) { + return with(key, encodeValue(value)); } - public T with(String key, double value) { - return self(options.with(key, encodeValue(value))); + public final T with(String key, double value) { + return with(key, encodeValue(value)); } - public T with(String key, Field value) { - return self(options.with(key, value.toProto())); + public final T with(String key, Field value) { + return with(key, value.toProto()); } - protected T with(String key, List expressions) { + 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 T with(String key, AbstractOptions subSection) { + protected final T with(String key, AbstractOptions subSection) { return self(options.with(key, subSection.options)); } - public T withSection(String key, GenericOptions subSection) { + public final T withSection(String key, GenericOptions subSection) { return with(key, subSection); } - ImmutableMap toMap() { + final ImmutableMap toMap() { return options.options; } } diff --git a/google-cloud-firestore/src/main/java/com/google/cloud/firestore/pipeline/stages/ExecuteOptions.java b/google-cloud-firestore/src/main/java/com/google/cloud/firestore/pipeline/stages/AggregateHints.java similarity index 65% rename from google-cloud-firestore/src/main/java/com/google/cloud/firestore/pipeline/stages/ExecuteOptions.java rename to google-cloud-firestore/src/main/java/com/google/cloud/firestore/pipeline/stages/AggregateHints.java index e0af34923..8ca2a3dd1 100644 --- a/google-cloud-firestore/src/main/java/com/google/cloud/firestore/pipeline/stages/ExecuteOptions.java +++ b/google-cloud-firestore/src/main/java/com/google/cloud/firestore/pipeline/stages/AggregateHints.java @@ -16,16 +16,20 @@ package com.google.cloud.firestore.pipeline.stages; -public class ExecuteOptions extends AbstractOptions { +public final class AggregateHints extends AbstractOptions { - public static ExecuteOptions DEFAULT = new ExecuteOptions(InternalOptions.EMPTY); + public static AggregateHints DEFAULT = new AggregateHints(InternalOptions.EMPTY); - ExecuteOptions(InternalOptions options) { + public AggregateHints(InternalOptions options) { super(options); } @Override - ExecuteOptions self(InternalOptions options) { - return new ExecuteOptions(options); + 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 index 64b7b067b..69f576d6d 100644 --- 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 @@ -16,7 +16,7 @@ package com.google.cloud.firestore.pipeline.stages; -public class AggregateOptions extends AbstractOptions { +public final class AggregateOptions extends AbstractOptions { public static AggregateOptions DEFAULT = new AggregateOptions(InternalOptions.EMPTY); @@ -28,4 +28,8 @@ public AggregateOptions(InternalOptions options) { AggregateOptions self(InternalOptions options) { return new AggregateOptions(options); } + + public AggregateOptions withHints(AggregateHints hints) { + return with("hints", hints); + } } 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 index 1abb2c62a..44f82baf1 100644 --- 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 @@ -16,9 +16,7 @@ package com.google.cloud.firestore.pipeline.stages; -import com.google.cloud.firestore.pipeline.stages.CollectionOptions.Hints; - -public class CollectionGroupOptions extends AbstractOptions { +public final class CollectionGroupOptions extends AbstractOptions { public static final CollectionGroupOptions DEFAULT = new CollectionGroupOptions(InternalOptions.EMPTY); @@ -31,7 +29,7 @@ CollectionGroupOptions self(InternalOptions options) { return new CollectionGroupOptions(options); } - public CollectionGroupOptions withHints(Hints hints) { + 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..c96927974 --- /dev/null +++ b/google-cloud-firestore/src/main/java/com/google/cloud/firestore/pipeline/stages/CollectionHints.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.stages; + +public final class CollectionHints extends AbstractOptions { + + public static CollectionHints DEFAULT = new CollectionHints(InternalOptions.EMPTY); + + CollectionHints(InternalOptions options) { + super(options); + } + + @Override + CollectionHints self(InternalOptions options) { + return new CollectionHints(options); + } + + public CollectionHints withForceIndex(String value) { + return with("forceIndex", 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 index e1596b688..9e9006a78 100644 --- 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 @@ -16,10 +16,7 @@ package com.google.cloud.firestore.pipeline.stages; -import com.google.cloud.firestore.pipeline.expressions.Field; -import java.util.Arrays; - -public class CollectionOptions extends AbstractOptions { +public final class CollectionOptions extends AbstractOptions { public static final CollectionOptions DEFAULT = new CollectionOptions(InternalOptions.EMPTY); @@ -32,25 +29,8 @@ CollectionOptions self(InternalOptions options) { return new CollectionOptions(options); } - public CollectionOptions withHints(Hints hints) { + public CollectionOptions withHints(CollectionHints hints) { return with("hints", hints); } - public static class Hints extends AbstractOptions { - - public static Hints DEFAULT = new Hints(InternalOptions.EMPTY); - - Hints(InternalOptions options) { - super(options); - } - - @Override - Hints self(InternalOptions options) { - return new Hints(options); - } - - public Hints withIgnoreIndexFields(Field... fields) { - return with("ignore_index_fields", Arrays.asList(fields)); - } - } } 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 index 1ddbea581..607e80f12 100644 --- 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 @@ -20,7 +20,7 @@ import com.google.cloud.firestore.pipeline.expressions.Field; @BetaApi -public class FindNearestOptions extends AbstractOptions { +public final class FindNearestOptions extends AbstractOptions { public static FindNearestOptions DEFAULT = new FindNearestOptions(InternalOptions.EMPTY); diff --git a/google-cloud-firestore/src/main/java/com/google/cloud/firestore/pipeline/stages/GenericOptions.java b/google-cloud-firestore/src/main/java/com/google/cloud/firestore/pipeline/stages/GenericOptions.java index 67a7795ea..f05e5f0ac 100644 --- a/google-cloud-firestore/src/main/java/com/google/cloud/firestore/pipeline/stages/GenericOptions.java +++ b/google-cloud-firestore/src/main/java/com/google/cloud/firestore/pipeline/stages/GenericOptions.java @@ -18,7 +18,6 @@ import static com.google.cloud.firestore.PipelineUtils.encodeValue; -import com.google.cloud.Timestamp; import com.google.cloud.firestore.pipeline.expressions.Field; public final class GenericOptions extends AbstractOptions { diff --git a/google-cloud-firestore/src/main/java/com/google/cloud/firestore/pipeline/stages/PipelineOptions.java b/google-cloud-firestore/src/main/java/com/google/cloud/firestore/pipeline/stages/PipelineOptions.java new file mode 100644 index 000000000..f75bffd54 --- /dev/null +++ b/google-cloud-firestore/src/main/java/com/google/cloud/firestore/pipeline/stages/PipelineOptions.java @@ -0,0 +1,56 @@ +/* + * 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 class PipelineOptions extends AbstractOptions { + + public static PipelineOptions DEFAULT = new PipelineOptions(InternalOptions.EMPTY) + .withExecutionMode("execute"); + + PipelineOptions(InternalOptions options) { + super(options); + } + + @Override + PipelineOptions self(InternalOptions options) { + return new PipelineOptions(options); + } + + public final PipelineOptions withExplainExecutionMode() { + return withExecutionMode("explain"); + } + + public PipelineOptions withProfileExecutionMode() { + return withExecutionMode("profile"); + } + + private PipelineOptions withExecutionMode(String mode) { + return with("execution_mode", mode); + } + + public PipelineOptions withIndexRecommendationEnabled() { + return with("index_recommendation", true); + } + + public PipelineOptions withShowAlternativePlanEnabled() { + return with("show_alternative_plans", true); + } + + public PipelineOptions withRedactEnabled() { + return with("redact", true); + } +} 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 index 75d39f8e7..c57a59d7a 100644 --- 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 @@ -16,7 +16,7 @@ package com.google.cloud.firestore.pipeline.stages; -public class SampleOptions extends AbstractOptions { +public final class SampleOptions extends AbstractOptions { public static SampleOptions DEFAULT = new SampleOptions(InternalOptions.EMPTY); 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 index 3365e042f..d54a650f4 100644 --- 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 @@ -20,7 +20,7 @@ import com.google.firestore.v1.Value; import java.util.Collections; -public class Union extends Stage { +public final class Union extends Stage { private final Pipeline other; 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 index caf83793c..3d7a3cfee 100644 --- 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 @@ -23,7 +23,7 @@ import com.google.firestore.v1.Value; import javax.annotation.Nonnull; -public class Unnest extends Stage { +public final class Unnest extends Stage { private final Field field; private final String 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 index e744875ce..59ab0cb2d 100644 --- 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 @@ -18,7 +18,7 @@ import javax.annotation.Nonnull; -public class UnnestOptions extends AbstractOptions { +public final class UnnestOptions extends AbstractOptions { public static UnnestOptions DEFAULT = new UnnestOptions(InternalOptions.EMPTY); 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 index c610110ba..7283f8920 100644 --- 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 @@ -41,6 +41,7 @@ import static com.google.common.truth.Truth.assertThat; import static org.junit.Assert.assertThrows; +import com.google.api.core.ApiFuture; import com.google.cloud.firestore.CollectionReference; import com.google.cloud.firestore.LocalFirestoreHelper; import com.google.cloud.firestore.Pipeline; @@ -49,7 +50,12 @@ import com.google.cloud.firestore.pipeline.expressions.Field; import com.google.cloud.firestore.pipeline.expressions.Function; 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.PipelineOptions; +import com.google.cloud.firestore.pipeline.stages.PipelineOptions.ExecutionMode; import com.google.cloud.firestore.pipeline.stages.Sample; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; @@ -1163,4 +1169,29 @@ public void testUnnest() throws Exception { assertThat(results).hasSize(3); } + + @Test + public void testOptions() { + // This is just example of execute and stage options. + // Nothing is executed against firestore. + + PipelineOptions opts = PipelineOptions.DEFAULT + .withIndexRecommendationEnabled() + .withExplainExecutionMode(); + + Pipeline pipeline = firestore.pipeline() + .collection( + "/k", + CollectionHints.DEFAULT.withForceIndex("abcdef") + ) + .aggregate( + Aggregate + .withAccumulators(avg("rating").as("avg_rating")) + .withGroups("genre") + .withOptions(AggregateOptions.DEFAULT + .withHints(AggregateHints.DEFAULT.withForceStreamableEnabled())) + ); + + pipeline.execute(opts); + } } From 6da47097a1d5be85935d0b68d5a3004a8a09ce7c Mon Sep 17 00:00:00 2001 From: Tom Andersen Date: Wed, 18 Dec 2024 12:18:24 -0500 Subject: [PATCH 5/7] Changes from feedback --- .../com/google/cloud/firestore/Pipeline.java | 28 ----------------- .../cloud/firestore/PipelineSource.java | 6 ---- .../pipeline/stages/PipelineOptions.java | 31 +++++++++++-------- .../cloud/firestore/it/ITPipelineTest.java | 5 +-- 4 files changed, 21 insertions(+), 49 deletions(-) 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 index c7522b265..929549e41 100644 --- 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 @@ -439,34 +439,6 @@ public Pipeline aggregate(Aggregate aggregate) { return append(aggregate); } - - /** - * 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 ExprWithAlias} expressions which are typically results of - * calling {@link Expr#as(String)} on {@link Accumulator} instances. - * - *

Example: - * - *

{@code
-   * // Calculate the average rating and the total number of books
-   * firestore.pipeline().collection("books")
-   *     .aggregate(
-   *         Field.of("rating").avg().as("averageRating"),
-   *         countAll().as("totalBooks")
-   *     );
-   * }
- * - * @param accumulators The {@link ExprWithAlias} expressions, each wrapping an {@link Accumulator} - * 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(AggregateOptions options, ExprWithAlias... accumulators) { - return append(Aggregate.withAccumulators(accumulators).withOptions(options)); - } - /** * Returns a set of distinct field values from the inputs to this stage. * 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 index 14fc0f852..18fbeff93 100644 --- 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 @@ -74,12 +74,6 @@ public Pipeline collection(@Nonnull String path, CollectionOptions options) { return new Pipeline(this.rpcContext, new Collection(path, options)); } - @Nonnull - @BetaApi - public Pipeline collection(@Nonnull String path, CollectionHints hints) { - return new Pipeline(this.rpcContext, new Collection(path, CollectionOptions.DEFAULT.withHints(hints))); - } - /** * Creates a new {@link Pipeline} that operates on all documents in a collection group. * diff --git a/google-cloud-firestore/src/main/java/com/google/cloud/firestore/pipeline/stages/PipelineOptions.java b/google-cloud-firestore/src/main/java/com/google/cloud/firestore/pipeline/stages/PipelineOptions.java index f75bffd54..591c7b465 100644 --- a/google-cloud-firestore/src/main/java/com/google/cloud/firestore/pipeline/stages/PipelineOptions.java +++ b/google-cloud-firestore/src/main/java/com/google/cloud/firestore/pipeline/stages/PipelineOptions.java @@ -16,10 +16,23 @@ package com.google.cloud.firestore.pipeline.stages; -public class PipelineOptions extends AbstractOptions { +import com.google.cloud.firestore.PipelineUtils; +import com.google.firestore.v1.Value; - public static PipelineOptions DEFAULT = new PipelineOptions(InternalOptions.EMPTY) - .withExecutionMode("execute"); +public final class PipelineOptions extends AbstractOptions { + + public static PipelineOptions DEFAULT = new PipelineOptions(InternalOptions.EMPTY); + + public enum ExecutionMode { + EXECUTE("execute"), + EXPLAIN("explain"), + PROFILE("profile"); + + private final Value value; + ExecutionMode(String profile) { + value = PipelineUtils.encodeValue(profile); + } + } PipelineOptions(InternalOptions options) { super(options); @@ -30,16 +43,8 @@ PipelineOptions self(InternalOptions options) { return new PipelineOptions(options); } - public final PipelineOptions withExplainExecutionMode() { - return withExecutionMode("explain"); - } - - public PipelineOptions withProfileExecutionMode() { - return withExecutionMode("profile"); - } - - private PipelineOptions withExecutionMode(String mode) { - return with("execution_mode", mode); + public PipelineOptions withExecutionMode(ExecutionMode mode) { + return with("execution_mode", mode.value); } public PipelineOptions withIndexRecommendationEnabled() { 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 index 7283f8920..c06164bbb 100644 --- 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 @@ -1177,12 +1177,13 @@ public void testOptions() { PipelineOptions opts = PipelineOptions.DEFAULT .withIndexRecommendationEnabled() - .withExplainExecutionMode(); + .withExecutionMode(ExecutionMode.PROFILE); Pipeline pipeline = firestore.pipeline() .collection( "/k", - CollectionHints.DEFAULT.withForceIndex("abcdef") + // Remove Hints overload - can be added later. + CollectionOptions.DEFAULT.withHints(CollectionHints.DEFAULT.withForceIndex("abcdef")) ) .aggregate( Aggregate From 94e5750991b90d56972390ce1a88fb155d493212 Mon Sep 17 00:00:00 2001 From: Tom Andersen Date: Wed, 18 Dec 2024 12:42:06 -0500 Subject: [PATCH 6/7] Expand example and fix. --- .../com/google/cloud/firestore/Pipeline.java | 20 +++++++---------- .../pipeline/stages/FindNearestOptions.java | 4 ++++ .../cloud/firestore/it/ITPipelineTest.java | 22 +++++++++++++++---- 3 files changed, 30 insertions(+), 16 deletions(-) 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 index 929549e41..4ca0f19b3 100644 --- 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 @@ -505,12 +505,10 @@ public Pipeline distinct(Selectable... selectables) { *
{@code
    * // Find books with similar "topicVectors" to the given targetVector
    * firestore.pipeline().collection("books")
-   *     .findNearest("topicVectors", targetVector, FindNearest.DistanceMeasure.cosine(),
-   *        FindNearestOptions
-   *          .builder()
-   *          .limit(10)
-   *          .distanceField("distance")
-   *          .build());
+   *     .findNearest("topicVectors", targetVector, FindNearest.DistanceMeasure.COSINE,
+   *        FindNearestOptions.DEFAULT
+   *          .withLimit(10)
+   *          .withDistanceField("distance"));
    * }
* * @param fieldName The name of the field containing the vector data. This field should store @@ -544,12 +542,10 @@ public Pipeline findNearest( * // Find books with similar "topicVectors" to the given targetVector * firestore.pipeline().collection("books") * .findNearest( - * FindNearest.of(Field.of("topicVectors"), targetVector, FindNearest.DistanceMeasure.cosine()), - * FindNearest - * .builder() - * .limit(10) - * .distanceField("distance") - * .build()); + * FindNearest.of(Field.of("topicVectors"), targetVector, FindNearest.DistanceMeasure.COSINE), + * FindNearestOptions.DEFAULT + * .withLimit(10) + * .withDistanceField("distance")); * } * * @param property The expression that evaluates to a vector value using the stage inputs. 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 index 607e80f12..03c0652c0 100644 --- 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 @@ -40,4 +40,8 @@ public FindNearestOptions withLimit(long limit) { public FindNearestOptions withDistanceField(Field distanceField) { return with("distance_field", distanceField); } + + public FindNearestOptions withDistanceField(String distanceField) { + return withDistanceField(Field.of(distanceField)); + } } 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 index c06164bbb..822d21834 100644 --- 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 @@ -54,6 +54,8 @@ 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.FindNearest; +import com.google.cloud.firestore.pipeline.stages.FindNearestOptions; import com.google.cloud.firestore.pipeline.stages.PipelineOptions; import com.google.cloud.firestore.pipeline.stages.PipelineOptions.ExecutionMode; import com.google.cloud.firestore.pipeline.stages.Sample; @@ -1173,24 +1175,36 @@ public void testUnnest() throws Exception { @Test public void testOptions() { // This is just example of execute and stage options. - // Nothing is executed against firestore. - PipelineOptions opts = PipelineOptions.DEFAULT .withIndexRecommendationEnabled() .withExecutionMode(ExecutionMode.PROFILE); + double[] vector = {1.0, 2.0, 3.0}; + Pipeline pipeline = firestore.pipeline() .collection( "/k", // Remove Hints overload - can be added later. - CollectionOptions.DEFAULT.withHints(CollectionHints.DEFAULT.withForceIndex("abcdef")) + CollectionOptions.DEFAULT + .withHints(CollectionHints.DEFAULT + .withForceIndex("abcdef") + .with("foo", "bar")) + .with("foo", "bar") ) + .findNearest("topicVectors", vector, FindNearest.DistanceMeasure.COSINE, + FindNearestOptions.DEFAULT + .withLimit(10) + .withDistanceField("distance") + .with("foo", "bar")) .aggregate( Aggregate .withAccumulators(avg("rating").as("avg_rating")) .withGroups("genre") .withOptions(AggregateOptions.DEFAULT - .withHints(AggregateHints.DEFAULT.withForceStreamableEnabled())) + .withHints(AggregateHints.DEFAULT + .withForceStreamableEnabled() + .with("foo", "bar")) + .with("foo", "bar")) ); pipeline.execute(opts); From dc38135d3d81d638c7cd36f1e32c9842010e68d4 Mon Sep 17 00:00:00 2001 From: Tom Andersen Date: Fri, 20 Dec 2024 11:17:54 -0500 Subject: [PATCH 7/7] Comments --- .../cloud/firestore/pipeline/stages/AbstractOptions.java | 9 +++++++++ .../cloud/firestore/pipeline/stages/InternalOptions.java | 8 ++++++++ 2 files changed, 17 insertions(+) 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 index 8e6364d52..2d7a64a3f 100644 --- 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 @@ -28,6 +28,15 @@ 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; 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 index 618516e6a..bad786c64 100644 --- 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 @@ -21,6 +21,14 @@ 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());