diff --git a/java-firestore/google-cloud-firestore/src/main/java/com/google/cloud/firestore/AggregateQuery.java b/java-firestore/google-cloud-firestore/src/main/java/com/google/cloud/firestore/AggregateQuery.java index dc6962ff90ff..014975be1ba0 100644 --- a/java-firestore/google-cloud-firestore/src/main/java/com/google/cloud/firestore/AggregateQuery.java +++ b/java-firestore/google-cloud-firestore/src/main/java/com/google/cloud/firestore/AggregateQuery.java @@ -20,6 +20,7 @@ import static com.google.cloud.firestore.telemetry.TraceUtil.ATTRIBUTE_KEY_ATTEMPT; import com.google.api.core.ApiFuture; +import com.google.api.core.BetaApi; import com.google.api.core.InternalExtensionOnly; import com.google.api.core.SettableApiFuture; import com.google.api.gax.rpc.ResponseObserver; @@ -35,6 +36,7 @@ import com.google.cloud.firestore.telemetry.TraceUtil.Scope; import com.google.cloud.firestore.v1.FirestoreSettings; import com.google.common.collect.ImmutableMap; +import com.google.firestore.v1.RequestOptions; import com.google.firestore.v1.RunAggregationQueryRequest; import com.google.firestore.v1.RunAggregationQueryResponse; import com.google.firestore.v1.RunQueryRequest; @@ -100,7 +102,20 @@ Pipeline pipeline() { */ @Nonnull public ApiFuture get() { - return get(null, null); + return get(null, null, null, null); + } + + /** + * Executes this query with execution options. + * + * @param executionOptions Options for executing the request. + * @return An {@link ApiFuture} that will be resolved with the results of the query. + */ + @BetaApi + @Nonnull + public ApiFuture get( + @Nonnull FirestoreExecutionOptions executionOptions) { + return get(null, null, executionOptions.getExplainOptions(), executionOptions); } /** @@ -113,6 +128,35 @@ public ApiFuture get() { */ @Nonnull public ApiFuture> explain(ExplainOptions options) { + return explain(options, null); + } + + /** + * Plans and optionally executes this query with execution options. + * + * @param executionOptions Options for executing the request. + * @return An ApiFuture that will be resolved with the planner information, statistics from the + * query execution (if any), and the query results (if any). + */ + @BetaApi + @Nonnull + public ApiFuture> explain( + @Nonnull FirestoreExecutionOptions executionOptions) { + return explain(executionOptions.getExplainOptions(), executionOptions); + } + + /** + * Plans and optionally executes this query with explain options and execution options. + * + * @param options The options for explain. + * @param executionOptions Options for executing the request. + * @return An ApiFuture that will be resolved with the planner information, statistics from the + * query execution (if any), and the query results (if any). + */ + @BetaApi + @Nonnull + public ApiFuture> explain( + @Nullable ExplainOptions options, @Nullable FirestoreExecutionOptions executionOptions) { TraceUtil.Span span = getTraceUtil().startSpan(TelemetryConstants.METHOD_NAME_AGGREGATION_QUERY_GET); @@ -125,8 +169,11 @@ public ApiFuture> explain(ExplainOptions /* transactionId= */ null, /* readTime= */ null, /* startTimeNanos= */ query.rpcContext.getClock().nanoTime(), - /* explainOptions= */ options, - metricsContext); + /* explainOptions= */ options != null + ? options + : (executionOptions != null ? executionOptions.getExplainOptions() : null), + metricsContext, + executionOptions); runQuery(responseDeliverer, /* attempt */ 0); ApiFuture> result = responseDeliverer.getFuture(); span.endAtFuture(result); @@ -140,6 +187,15 @@ public ApiFuture> explain(ExplainOptions @Nonnull ApiFuture get( @Nullable final ByteString transactionId, @Nullable com.google.protobuf.Timestamp readTime) { + return get(transactionId, readTime, null, null); + } + + @Nonnull + ApiFuture get( + @Nullable final ByteString transactionId, + @Nullable com.google.protobuf.Timestamp readTime, + @Nullable ExplainOptions explainOptions, + @Nullable FirestoreExecutionOptions executionOptions) { TraceUtil.Span span = getTraceUtil() .startSpan( @@ -159,7 +215,8 @@ ApiFuture get( transactionId, readTime, /* startTimeNanos= */ query.rpcContext.getClock().nanoTime(), - metricsContext); + metricsContext, + executionOptions); runQuery(responseDeliverer, /* attempt= */ 0); ApiFuture result = responseDeliverer.getFuture(); span.endAtFuture(result); @@ -175,7 +232,8 @@ private void runQuery(ResponseDeliverer responseDeliverer, int attempt) { toProto( responseDeliverer.getTransactionId(), responseDeliverer.getReadTime(), - responseDeliverer.getExplainOptions()); + responseDeliverer.getExplainOptions(), + responseDeliverer.getExecutionOptions()); AggregateQueryResponseObserver responseObserver = new AggregateQueryResponseObserver(responseDeliverer, attempt); ServerStreamingCallable callable = @@ -197,16 +255,24 @@ private abstract static class ResponseDeliverer { private final long startTimeNanos; private final SettableApiFuture future = SettableApiFuture.create(); private MetricsContext metricsContext; + private final @Nullable FirestoreExecutionOptions executionOptions; ResponseDeliverer( @Nullable ByteString transactionId, @Nullable com.google.protobuf.Timestamp readTime, long startTimeNanos, - MetricsContext metricsContext) { + MetricsContext metricsContext, + @Nullable FirestoreExecutionOptions executionOptions) { this.transactionId = transactionId; this.readTime = readTime; this.startTimeNanos = startTimeNanos; this.metricsContext = metricsContext; + this.executionOptions = executionOptions; + } + + @Nullable + FirestoreExecutionOptions getExecutionOptions() { + return executionOptions; } @Nullable @@ -265,8 +331,9 @@ private class AggregateQueryResponseDeliverer extends ResponseDeliverer observer) { + getPartitions(desiredPartitionCount, observer, null); + } + + /** + * Partitions a query by returning partition cursors that can be used to run the query in + * parallel, with execution options. + * + * @param desiredPartitionCount The desired maximum number of partition points. + * @param observer a stream observer that receives the result of the Partition request. + * @param executionOptions Options for executing the request. + */ + @BetaApi + public void getPartitions( + long desiredPartitionCount, + final ApiStreamObserver observer, + @Nullable FirestoreExecutionOptions executionOptions) { if (desiredPartitionCount == 1) { // Short circuit if the user only requested a single partition. observer.onNext(new QueryPartition(partitionQuery, null, null)); } else { - PartitionQueryRequest request = buildRequest(desiredPartitionCount); + PartitionQueryRequest request = buildRequest(desiredPartitionCount, executionOptions); final PartitionQueryPagedResponse response; try { @@ -100,12 +118,24 @@ public void getPartitions( } public ApiFuture> getPartitions(long desiredPartitionCount) { + return getPartitions(desiredPartitionCount, (FirestoreExecutionOptions) null); + } + + /** + * Partitions a query by returning partition cursors with execution options. + * + * @param desiredPartitionCount The desired maximum number of partition points. + * @param executionOptions Options for executing the request. + */ + @BetaApi + public ApiFuture> getPartitions( + long desiredPartitionCount, @Nullable FirestoreExecutionOptions executionOptions) { if (desiredPartitionCount == 1) { // Short circuit if the user only requested a single partition. return ApiFutures.immediateFuture( Collections.singletonList(new QueryPartition(partitionQuery, null, null))); } else { - PartitionQueryRequest request = buildRequest(desiredPartitionCount); + PartitionQueryRequest request = buildRequest(desiredPartitionCount, executionOptions); TraceUtil.Span span = rpcContext @@ -152,7 +182,8 @@ public ApiFuture> getPartitions(long desiredPartitionCount) } } - private PartitionQueryRequest buildRequest(long desiredPartitionCount) { + private PartitionQueryRequest buildRequest( + long desiredPartitionCount, @Nullable FirestoreExecutionOptions executionOptions) { Preconditions.checkArgument( desiredPartitionCount > 0, "Desired partition count must be one or greater"); @@ -163,6 +194,12 @@ private PartitionQueryRequest buildRequest(long desiredPartitionCount) { // Since we are always returning an extra partition (with en empty endBefore cursor), we // reduce the desired partition count by one. request.setPartitionCount(desiredPartitionCount - 1); + RequestOptions requestOptions = + RequestOptionsHelper.createRequestOptions( + rpcContext.getFirestore().getOptions(), executionOptions); + if (!requestOptions.equals(RequestOptions.getDefaultInstance())) { + request.setRequestOptions(requestOptions); + } return request.build(); } diff --git a/java-firestore/google-cloud-firestore/src/main/java/com/google/cloud/firestore/CollectionReference.java b/java-firestore/google-cloud-firestore/src/main/java/com/google/cloud/firestore/CollectionReference.java index 75954d82de3a..6ce89a004a49 100644 --- a/java-firestore/google-cloud-firestore/src/main/java/com/google/cloud/firestore/CollectionReference.java +++ b/java-firestore/google-cloud-firestore/src/main/java/com/google/cloud/firestore/CollectionReference.java @@ -18,6 +18,7 @@ import com.google.api.core.ApiFuture; import com.google.api.core.ApiFutures; +import com.google.api.core.BetaApi; import com.google.api.core.InternalExtensionOnly; import com.google.api.gax.rpc.ApiException; import com.google.api.gax.rpc.ApiExceptions; @@ -35,6 +36,7 @@ import com.google.firestore.v1.Document; import com.google.firestore.v1.DocumentMask; import com.google.firestore.v1.ListDocumentsRequest; +import com.google.firestore.v1.RequestOptions; import java.util.Iterator; import java.util.Map; import javax.annotation.Nonnull; @@ -132,6 +134,19 @@ public DocumentReference document(@Nonnull String childPath) { */ @Nonnull public Iterable listDocuments() { + return listDocuments((FirestoreExecutionOptions) null); + } + + /** + * Retrieves the list of documents in this collection with execution options. + * + * @param executionOptions Options for executing the request. + * @return The list of documents in this collection. + */ + @BetaApi + @Nonnull + public Iterable listDocuments( + @Nullable FirestoreExecutionOptions executionOptions) { TraceUtil.Span span = rpcContext .getFirestore() @@ -152,6 +167,12 @@ public Iterable listDocuments() { request.setCollectionId(options.getCollectionId()); request.setMask(DocumentMask.getDefaultInstance()); request.setShowMissing(true); + RequestOptions requestOptions = + RequestOptionsHelper.createRequestOptions( + rpcContext.getFirestore().getOptions(), executionOptions); + if (!requestOptions.equals(RequestOptions.getDefaultInstance())) { + request.setRequestOptions(requestOptions); + } final ListDocumentsPagedResponse response; FirestoreRpc client = rpcContext.getClient(); UnaryCallable callable = @@ -209,6 +230,24 @@ public void remove() { */ @Nonnull public ApiFuture add(@Nonnull final Map fields) { + return add(fields, (FirestoreExecutionOptions) null); + } + + /** + * Adds a new document to this collection with the specified data and execution options, assigning + * it a document ID automatically. + * + * @param fields A Map containing the data for the new document. + * @param executionOptions Options for executing the request. + * @return An ApiFuture that will be resolved with the DocumentReference of the newly created + * document. + * @see #document() + */ + @BetaApi + @Nonnull + public ApiFuture add( + @Nonnull final Map fields, + @Nullable FirestoreExecutionOptions executionOptions) { TraceUtil.Span span = rpcContext .getFirestore() @@ -225,7 +264,10 @@ public ApiFuture add(@Nonnull final Map field try (Scope ignored = span.makeCurrent()) { final DocumentReference documentReference = document(); - ApiFuture createFuture = documentReference.create(fields); + ApiFuture createFuture = + executionOptions != null + ? documentReference.create(fields, executionOptions) + : documentReference.create(fields); ApiFuture result = ApiFutures.transform( createFuture, writeResult -> documentReference, MoreExecutors.directExecutor()); @@ -249,12 +291,28 @@ public ApiFuture add(@Nonnull final Map field * @see #document() */ public ApiFuture add(Object pojo) { + return add(pojo, (FirestoreExecutionOptions) null); + } + + /** + * Adds a new document to this collection with the specified POJO as contents and execution + * options, assigning it a document ID automatically. + * + * @param pojo The POJO that will be used to populate the contents of the document + * @param executionOptions Options for executing the request. + * @return An ApiFuture that will be resolved with the DocumentReference of the newly created + * document. + * @see #document() + */ + @BetaApi + public ApiFuture add( + Object pojo, @Nullable FirestoreExecutionOptions executionOptions) { Object converted = CustomClassMapper.convertToPlainJavaTypes(pojo); if (!(converted instanceof Map)) { throw FirestoreException.forInvalidArgument( "Can't set a document's data to an array or primitive"); } - return add((Map) converted); + return add((Map) converted, executionOptions); } /** Returns a resource path pointing to this collection. */ diff --git a/java-firestore/google-cloud-firestore/src/main/java/com/google/cloud/firestore/DocumentReference.java b/java-firestore/google-cloud-firestore/src/main/java/com/google/cloud/firestore/DocumentReference.java index 2b0cc1ddc939..5e353e396d62 100644 --- a/java-firestore/google-cloud-firestore/src/main/java/com/google/cloud/firestore/DocumentReference.java +++ b/java-firestore/google-cloud-firestore/src/main/java/com/google/cloud/firestore/DocumentReference.java @@ -18,6 +18,7 @@ import com.google.api.core.ApiFuture; import com.google.api.core.ApiFutures; +import com.google.api.core.BetaApi; import com.google.api.core.InternalExtensionOnly; import com.google.api.gax.rpc.ApiException; import com.google.api.gax.rpc.ApiExceptions; @@ -30,6 +31,7 @@ import com.google.cloud.firestore.v1.FirestoreClient.ListCollectionIdsPagedResponse; import com.google.common.util.concurrent.MoreExecutors; import com.google.firestore.v1.ListCollectionIdsRequest; +import com.google.firestore.v1.RequestOptions; import java.util.Iterator; import java.util.List; import java.util.Map; @@ -157,6 +159,20 @@ private MetricsUtil getMetricsUtil() { */ @Nonnull public ApiFuture create(@Nonnull Map fields) { + return create(fields, (FirestoreExecutionOptions) null); + } + + /** + * Creates a new Document at the DocumentReference's Location with execution options. + * + * @param fields A map of the fields and values for the document. + * @param executionOptions Options for executing the request. + * @return An ApiFuture that will be resolved when the write finishes. + */ + @BetaApi + @Nonnull + public ApiFuture create( + @Nonnull Map fields, @Nullable FirestoreExecutionOptions executionOptions) { TraceUtil.Span span = getTraceUtil().startSpan(TelemetryConstants.METHOD_NAME_DOC_REF_CREATE); MetricsContext metricsContext = @@ -164,7 +180,11 @@ public ApiFuture create(@Nonnull Map fields) { try (Scope ignored = span.makeCurrent()) { WriteBatch writeBatch = rpcContext.getFirestore().batch(); - ApiFuture result = extractFirst(writeBatch.create(this, fields).commit()); + ApiFuture result = + extractFirst( + executionOptions != null + ? writeBatch.create(this, fields).commit(executionOptions) + : writeBatch.create(this, fields).commit()); span.endAtFuture(result); metricsContext.recordLatencyAtFuture(MetricType.END_TO_END_LATENCY, result); return result; @@ -184,13 +204,31 @@ public ApiFuture create(@Nonnull Map fields) { */ @Nonnull public ApiFuture create(@Nonnull Object pojo) { + return create(pojo, (FirestoreExecutionOptions) null); + } + + /** + * Creates a new Document at the DocumentReference location with execution options. + * + * @param pojo The POJO that will be used to populate the document contents. + * @param executionOptions Options for executing the request. + * @return An ApiFuture that will be resolved when the write finishes. + */ + @BetaApi + @Nonnull + public ApiFuture create( + @Nonnull Object pojo, @Nullable FirestoreExecutionOptions executionOptions) { TraceUtil.Span span = getTraceUtil().startSpan(TelemetryConstants.METHOD_NAME_DOC_REF_CREATE); MetricsContext metricsContext = getMetricsUtil().createMetricsContext(TelemetryConstants.METHOD_NAME_DOC_REF_CREATE); try (Scope ignored = span.makeCurrent()) { WriteBatch writeBatch = rpcContext.getFirestore().batch(); - ApiFuture result = extractFirst(writeBatch.create(this, pojo).commit()); + ApiFuture result = + extractFirst( + executionOptions != null + ? writeBatch.create(this, pojo).commit(executionOptions) + : writeBatch.create(this, pojo).commit()); span.endAtFuture(result); metricsContext.recordLatencyAtFuture(MetricType.END_TO_END_LATENCY, result); return result; @@ -210,13 +248,31 @@ public ApiFuture create(@Nonnull Object pojo) { */ @Nonnull public ApiFuture set(@Nonnull Map fields) { + return set(fields, (FirestoreExecutionOptions) null); + } + + /** + * Overwrites the document referred to by this DocumentReference with execution options. + * + * @param fields A map of the fields and values for the document. + * @param executionOptions Options for executing the request. + * @return An ApiFuture that will be resolved when the write finishes. + */ + @BetaApi + @Nonnull + public ApiFuture set( + @Nonnull Map fields, @Nullable FirestoreExecutionOptions executionOptions) { TraceUtil.Span span = getTraceUtil().startSpan(TelemetryConstants.METHOD_NAME_DOC_REF_SET); MetricsContext metricsContext = getMetricsUtil().createMetricsContext(TelemetryConstants.METHOD_NAME_DOC_REF_SET); try (Scope ignored = span.makeCurrent()) { WriteBatch writeBatch = rpcContext.getFirestore().batch(); - ApiFuture result = extractFirst(writeBatch.set(this, fields).commit()); + ApiFuture result = + extractFirst( + executionOptions != null + ? writeBatch.set(this, fields).commit(executionOptions) + : writeBatch.set(this, fields).commit()); span.endAtFuture(result); metricsContext.recordLatencyAtFuture(MetricType.END_TO_END_LATENCY, result); return result; @@ -239,13 +295,34 @@ public ApiFuture set(@Nonnull Map fields) { @Nonnull public ApiFuture set( @Nonnull Map fields, @Nonnull SetOptions options) { + return set(fields, options, null); + } + + /** + * Writes to the document referred to by this DocumentReference with execution options. + * + * @param fields A map of the fields and values for the document. + * @param options An object to configure the set behavior. + * @param executionOptions Options for executing the request. + * @return An ApiFuture that will be resolved when the write finishes. + */ + @BetaApi + @Nonnull + public ApiFuture set( + @Nonnull Map fields, + @Nonnull SetOptions options, + @Nullable FirestoreExecutionOptions executionOptions) { TraceUtil.Span span = getTraceUtil().startSpan(TelemetryConstants.METHOD_NAME_DOC_REF_SET); MetricsContext metricsContext = getMetricsUtil().createMetricsContext(TelemetryConstants.METHOD_NAME_DOC_REF_SET); try (Scope ignored = span.makeCurrent()) { WriteBatch writeBatch = rpcContext.getFirestore().batch(); - ApiFuture result = extractFirst(writeBatch.set(this, fields, options).commit()); + ApiFuture result = + extractFirst( + executionOptions != null + ? writeBatch.set(this, fields, options).commit(executionOptions) + : writeBatch.set(this, fields, options).commit()); span.endAtFuture(result); metricsContext.recordLatencyAtFuture(MetricType.END_TO_END_LATENCY, result); return result; @@ -265,13 +342,31 @@ public ApiFuture set( */ @Nonnull public ApiFuture set(@Nonnull Object pojo) { + return set(pojo, (FirestoreExecutionOptions) null); + } + + /** + * Overwrites the document referred to by this DocumentReference with execution options. + * + * @param pojo The POJO that will be used to populate the document contents. + * @param executionOptions Options for executing the request. + * @return An ApiFuture that will be resolved when the write finishes. + */ + @BetaApi + @Nonnull + public ApiFuture set( + @Nonnull Object pojo, @Nullable FirestoreExecutionOptions executionOptions) { TraceUtil.Span span = getTraceUtil().startSpan(TelemetryConstants.METHOD_NAME_DOC_REF_SET); MetricsContext metricsContext = getMetricsUtil().createMetricsContext(TelemetryConstants.METHOD_NAME_DOC_REF_SET); try (Scope ignored = span.makeCurrent()) { WriteBatch writeBatch = rpcContext.getFirestore().batch(); - ApiFuture result = extractFirst(writeBatch.set(this, pojo).commit()); + ApiFuture result = + extractFirst( + executionOptions != null + ? writeBatch.set(this, pojo).commit(executionOptions) + : writeBatch.set(this, pojo).commit()); span.endAtFuture(result); metricsContext.recordLatencyAtFuture(MetricType.END_TO_END_LATENCY, result); return result; @@ -293,13 +388,34 @@ public ApiFuture set(@Nonnull Object pojo) { */ @Nonnull public ApiFuture set(@Nonnull Object pojo, @Nonnull SetOptions options) { + return set(pojo, options, null); + } + + /** + * Writes to the document referred to by this DocumentReference with execution options. + * + * @param pojo The POJO that will be used to populate the document contents. + * @param options An object to configure the set behavior. + * @param executionOptions Options for executing the request. + * @return An ApiFuture that will be resolved when the write finishes. + */ + @BetaApi + @Nonnull + public ApiFuture set( + @Nonnull Object pojo, + @Nonnull SetOptions options, + @Nullable FirestoreExecutionOptions executionOptions) { TraceUtil.Span span = getTraceUtil().startSpan(TelemetryConstants.METHOD_NAME_DOC_REF_SET); MetricsContext metricsContext = getMetricsUtil().createMetricsContext(TelemetryConstants.METHOD_NAME_DOC_REF_SET); try (Scope ignored = span.makeCurrent()) { WriteBatch writeBatch = rpcContext.getFirestore().batch(); - ApiFuture result = extractFirst(writeBatch.set(this, pojo, options).commit()); + ApiFuture result = + extractFirst( + executionOptions != null + ? writeBatch.set(this, pojo, options).commit(executionOptions) + : writeBatch.set(this, pojo, options).commit()); span.endAtFuture(result); metricsContext.recordLatencyAtFuture(MetricType.END_TO_END_LATENCY, result); return result; @@ -319,13 +435,31 @@ public ApiFuture set(@Nonnull Object pojo, @Nonnull SetOptions opti */ @Nonnull public ApiFuture update(@Nonnull Map fields) { + return update(fields, (FirestoreExecutionOptions) null); + } + + /** + * Updates fields in the document referred to by this DocumentReference with execution options. + * + * @param fields A Map containing the fields and values with which to update the document. + * @param executionOptions Options for executing the request. + * @return An ApiFuture that will be resolved when the write finishes. + */ + @BetaApi + @Nonnull + public ApiFuture update( + @Nonnull Map fields, @Nullable FirestoreExecutionOptions executionOptions) { TraceUtil.Span span = getTraceUtil().startSpan(TelemetryConstants.METHOD_NAME_DOC_REF_UPDATE); MetricsContext metricsContext = getMetricsUtil().createMetricsContext(TelemetryConstants.METHOD_NAME_DOC_REF_UPDATE); try (Scope ignored = span.makeCurrent()) { WriteBatch writeBatch = rpcContext.getFirestore().batch(); - ApiFuture result = extractFirst(writeBatch.update(this, fields).commit()); + ApiFuture result = + extractFirst( + executionOptions != null + ? writeBatch.update(this, fields).commit(executionOptions) + : writeBatch.update(this, fields).commit()); span.endAtFuture(result); metricsContext.recordLatencyAtFuture(MetricType.END_TO_END_LATENCY, result); return result; @@ -346,6 +480,23 @@ public ApiFuture update(@Nonnull Map fields) { */ @Nonnull public ApiFuture update(@Nonnull Map fields, Precondition options) { + return update(fields, options, null); + } + + /** + * Updates fields in the document referred to by this DocumentReference with execution options. + * + * @param fields A map containing the fields and values with which to update the document. + * @param options Preconditions to enforce on this update. + * @param executionOptions Options for executing the request. + * @return An ApiFuture that will be resolved when the write finishes. + */ + @BetaApi + @Nonnull + public ApiFuture update( + @Nonnull Map fields, + Precondition options, + @Nullable FirestoreExecutionOptions executionOptions) { TraceUtil.Span span = getTraceUtil().startSpan(TelemetryConstants.METHOD_NAME_DOC_REF_UPDATE); MetricsContext metricsContext = getMetricsUtil().createMetricsContext(TelemetryConstants.METHOD_NAME_DOC_REF_UPDATE); @@ -353,7 +504,10 @@ public ApiFuture update(@Nonnull Map fields, Precon try (Scope ignored = span.makeCurrent()) { WriteBatch writeBatch = rpcContext.getFirestore().batch(); ApiFuture result = - extractFirst(writeBatch.update(this, fields, options).commit()); + extractFirst( + executionOptions != null + ? writeBatch.update(this, fields, options).commit(executionOptions) + : writeBatch.update(this, fields, options).commit()); span.endAtFuture(result); metricsContext.recordLatencyAtFuture(MetricType.END_TO_END_LATENCY, result); return result; @@ -502,13 +656,24 @@ public ApiFuture update( */ @Nonnull public ApiFuture delete(@Nonnull Precondition options) { + return delete(options, (FirestoreExecutionOptions) null); + } + + @BetaApi + @Nonnull + public ApiFuture delete( + @Nonnull Precondition options, @Nullable FirestoreExecutionOptions executionOptions) { TraceUtil.Span span = getTraceUtil().startSpan(TelemetryConstants.METHOD_NAME_DOC_REF_DELETE); MetricsContext metricsContext = getMetricsUtil().createMetricsContext(TelemetryConstants.METHOD_NAME_DOC_REF_DELETE); try (Scope ignored = span.makeCurrent()) { WriteBatch writeBatch = rpcContext.getFirestore().batch(); - ApiFuture result = extractFirst(writeBatch.delete(this, options).commit()); + ApiFuture result = + extractFirst( + executionOptions != null + ? writeBatch.delete(this, options).commit(executionOptions) + : writeBatch.delete(this, options).commit()); span.endAtFuture(result); metricsContext.recordLatencyAtFuture(MetricType.END_TO_END_LATENCY, result); return result; @@ -526,21 +691,13 @@ public ApiFuture delete(@Nonnull Precondition options) { */ @Nonnull public ApiFuture delete() { - TraceUtil.Span span = getTraceUtil().startSpan(TelemetryConstants.METHOD_NAME_DOC_REF_DELETE); - MetricsContext metricsContext = - getMetricsUtil().createMetricsContext(TelemetryConstants.METHOD_NAME_DOC_REF_DELETE); + return delete(Precondition.NONE, (FirestoreExecutionOptions) null); + } - try (Scope ignored = span.makeCurrent()) { - WriteBatch writeBatch = rpcContext.getFirestore().batch(); - ApiFuture result = extractFirst(writeBatch.delete(this).commit()); - span.endAtFuture(result); - metricsContext.recordLatencyAtFuture(MetricType.END_TO_END_LATENCY, result); - return result; - } catch (Exception error) { - span.end(error); - metricsContext.recordLatency(MetricType.END_TO_END_LATENCY, error); - throw error; - } + @BetaApi + @Nonnull + public ApiFuture delete(@Nullable FirestoreExecutionOptions executionOptions) { + return delete(Precondition.NONE, executionOptions); } /** @@ -552,20 +709,20 @@ public ApiFuture delete() { */ @Nonnull public ApiFuture get() { - TraceUtil.Span span = getTraceUtil().startSpan(TelemetryConstants.METHOD_NAME_DOC_REF_GET); - MetricsContext metricsContext = - getMetricsUtil().createMetricsContext(TelemetryConstants.METHOD_NAME_DOC_REF_GET); + return get((FieldMask) null, null); + } - try (Scope ignored = span.makeCurrent()) { - ApiFuture result = extractFirst(rpcContext.getFirestore().getAll(this)); - span.endAtFuture(result); - metricsContext.recordLatencyAtFuture(MetricType.END_TO_END_LATENCY, result); - return result; - } catch (Exception error) { - span.end(error); - metricsContext.recordLatency(MetricType.END_TO_END_LATENCY, error); - throw error; - } + /** + * Reads the document referenced by this DocumentReference with execution options. + * + * @param executionOptions Options for executing the request. + * @return An ApiFuture that will be resolved with the contents of the Document at this + * DocumentReference. + */ + @BetaApi + @Nonnull + public ApiFuture get(@Nonnull FirestoreExecutionOptions executionOptions) { + return get((FieldMask) null, executionOptions); } /** @@ -578,13 +735,31 @@ public ApiFuture get() { */ @Nonnull public ApiFuture get(FieldMask fieldMask) { + return get(fieldMask, null); + } + + /** + * Reads the document referenced by this DocumentReference with execution options. + * + * @param fieldMask A FieldMask object to retrieve the field value + * @param executionOptions Options for executing the request. + * @return An ApiFuture that will be resolved with the contents of the Document at this + * DocumentReference, or a failure if the document does not exist + */ + @BetaApi + @Nonnull + public ApiFuture get( + @Nullable FieldMask fieldMask, @Nullable FirestoreExecutionOptions executionOptions) { TraceUtil.Span span = getTraceUtil().startSpan(TelemetryConstants.METHOD_NAME_DOC_REF_GET); MetricsContext metricsContext = getMetricsUtil().createMetricsContext(TelemetryConstants.METHOD_NAME_DOC_REF_GET); try (Scope ignored = span.makeCurrent()) { ApiFuture result = - extractFirst(rpcContext.getFirestore().getAll(new DocumentReference[] {this}, fieldMask)); + extractFirst( + rpcContext + .getFirestore() + .getAll(new DocumentReference[] {this}, fieldMask, executionOptions)); span.endAtFuture(result); metricsContext.recordLatencyAtFuture(MetricType.END_TO_END_LATENCY, result); return result; @@ -603,6 +778,20 @@ public ApiFuture get(FieldMask fieldMask) { */ @Nonnull public Iterable listCollections() { + return listCollections(null); + } + + /** + * Fetches the subcollections that are direct children of this document with execution options. + * + * @param executionOptions Options for executing the request. + * @throws FirestoreException if the Iterable could not be initialized. + * @return An Iterable that can be used to fetch all subcollections. + */ + @BetaApi + @Nonnull + public Iterable listCollections( + @Nullable FirestoreExecutionOptions executionOptions) { TraceUtil.Span span = getTraceUtil().startSpan(TelemetryConstants.METHOD_NAME_DOC_REF_LIST_COLLECTIONS); MetricsContext metricsContext = @@ -612,6 +801,12 @@ public Iterable listCollections() { try (Scope ignored = span.makeCurrent()) { ListCollectionIdsRequest.Builder request = ListCollectionIdsRequest.newBuilder(); request.setParent(path.toString()); + RequestOptions requestOptions = + RequestOptionsHelper.createRequestOptions( + rpcContext.getFirestore().getOptions(), executionOptions); + if (!requestOptions.equals(RequestOptions.getDefaultInstance())) { + request.setRequestOptions(requestOptions); + } final ListCollectionIdsPagedResponse response; response = ApiExceptions.callAndTranslateApiException( diff --git a/java-firestore/google-cloud-firestore/src/main/java/com/google/cloud/firestore/Firestore.java b/java-firestore/google-cloud-firestore/src/main/java/com/google/cloud/firestore/Firestore.java index a5a4caad5144..6863ceaa0809 100644 --- a/java-firestore/google-cloud-firestore/src/main/java/com/google/cloud/firestore/Firestore.java +++ b/java-firestore/google-cloud-firestore/src/main/java/com/google/cloud/firestore/Firestore.java @@ -56,6 +56,18 @@ public interface Firestore extends Service, AutoCloseable { @Nonnull Iterable listCollections(); + /** + * Fetches the root collections that are associated with this Firestore database. + * + * @param executionOptions Options for executing the request. + * @throws FirestoreException if the Iterable could not be initialized. + * @return An Iterable that can be used to fetch all collections. + */ + @BetaApi + @Nonnull + Iterable listCollections( + @Nonnull FirestoreExecutionOptions executionOptions); + /** * Creates and returns a new {@link CollectionGroup} that includes all documents in the database * that are contained in a collection or subcollection with the given @code{collectionId}. @@ -159,6 +171,66 @@ ApiFuture runAsyncTransaction( @Nonnull final Transaction.AsyncFunction updateFunction, @Nonnull TransactionOptions transactionOptions); + /** + * Executes the given updateFunction with execution options and then attempts to commit the + * changes applied within the transaction. + * + * @param updateFunction The function to execute within the transaction context. + * @param executionOptions Options for executing the request. + * @return An ApiFuture that will be resolved with the result from updateFunction. + */ + @BetaApi + @Nonnull + ApiFuture runTransaction( + @Nonnull final Transaction.Function updateFunction, + @Nonnull FirestoreExecutionOptions executionOptions); + + /** + * Executes the given updateFunction with execution options and transaction options and then + * attempts to commit the changes applied within the transaction. + * + * @param updateFunction The function to execute within the transaction context. + * @param transactionOptions Options for the transaction. + * @param executionOptions Options for executing the request. + * @return An ApiFuture that will be resolved with the result from updateFunction. + */ + @BetaApi + @Nonnull + ApiFuture runTransaction( + @Nonnull final Transaction.Function updateFunction, + @Nonnull TransactionOptions transactionOptions, + @Nonnull FirestoreExecutionOptions executionOptions); + + /** + * Executes the given async updateFunction with execution options and then attempts to commit the + * changes applied within the transaction. + * + * @param updateFunction The function to execute within the transaction context. + * @param executionOptions Options for executing the request. + * @return An ApiFuture that will be resolved with the result from updateFunction. + */ + @BetaApi + @Nonnull + ApiFuture runAsyncTransaction( + @Nonnull final Transaction.AsyncFunction updateFunction, + @Nonnull FirestoreExecutionOptions executionOptions); + + /** + * Executes the given async updateFunction with execution options and transaction options and then + * attempts to commit the changes applied within the transaction. + * + * @param updateFunction The function to execute within the transaction context. + * @param transactionOptions Options for the transaction. + * @param executionOptions Options for executing the request. + * @return An ApiFuture that will be resolved with the result from updateFunction. + */ + @BetaApi + @Nonnull + ApiFuture runAsyncTransaction( + @Nonnull final Transaction.AsyncFunction updateFunction, + @Nonnull TransactionOptions transactionOptions, + @Nonnull FirestoreExecutionOptions executionOptions); + /** * Retrieves multiple documents from Firestore. * @@ -192,6 +264,51 @@ void getAll( @Nullable FieldMask fieldMask, final ApiStreamObserver responseObserver); + /** + * Retrieves multiple documents from Firestore with execution options. + * + * @param documentReferences Array with Document References to fetch. + * @param executionOptions Options for executing the request. + */ + @BetaApi + @Nonnull + ApiFuture> getAll( + @Nonnull DocumentReference[] documentReferences, + @Nonnull FirestoreExecutionOptions executionOptions); + + /** + * Retrieves multiple documents from Firestore, while optionally applying a field mask to reduce + * the amount of data transmitted, with execution options. + * + * @param documentReferences Array with Document References to fetch. + * @param fieldMask If set, specifies the subset of fields to return. + * @param executionOptions Options for executing the request. + */ + @BetaApi + @Nonnull + ApiFuture> getAll( + @Nonnull DocumentReference[] documentReferences, + @Nullable FieldMask fieldMask, + @Nonnull FirestoreExecutionOptions executionOptions); + + /** + * Retrieves multiple documents from Firestore while optionally applying a field mask to reduce + * the amount of data transmitted, with execution options. Returned documents will be out of + * order. + * + * @param documentReferences Array with Document References to fetch. + * @param fieldMask If not null, specifies the subset of fields to return. + * @param responseObserver The observer to be notified when {@link DocumentSnapshot} details + * arrive. + * @param executionOptions Options for executing the request. + */ + @BetaApi + void getAll( + @Nonnull DocumentReference[] documentReferences, + @Nullable FieldMask fieldMask, + final ApiStreamObserver responseObserver, + @Nonnull FirestoreExecutionOptions executionOptions); + /** * Gets a Firestore {@link WriteBatch} instance that can be used to combine multiple writes. * diff --git a/java-firestore/google-cloud-firestore/src/main/java/com/google/cloud/firestore/FirestoreExecutionOptions.java b/java-firestore/google-cloud-firestore/src/main/java/com/google/cloud/firestore/FirestoreExecutionOptions.java new file mode 100644 index 000000000000..d72402925674 --- /dev/null +++ b/java-firestore/google-cloud-firestore/src/main/java/com/google/cloud/firestore/FirestoreExecutionOptions.java @@ -0,0 +1,132 @@ +/* + * Copyright 2026 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.cloud.firestore; + +import com.google.api.core.BetaApi; +import com.google.cloud.firestore.models.RequestOptions; +import com.google.common.base.Objects; +import com.google.common.base.Preconditions; +import javax.annotation.Nonnull; +import javax.annotation.Nullable; + +/** + * Class representing options for query or request execution in Google Cloud Firestore. Combines + * {@link ExplainOptions} and {@link RequestOptions}. + */ +@BetaApi +public class FirestoreExecutionOptions { + + private final @Nullable ExplainOptions explainOptions; + private final RequestOptions requestOptions; + + private FirestoreExecutionOptions(Builder builder) { + this.explainOptions = builder.explainOptions; + this.requestOptions = builder.requestOptions; + } + + @Nullable + public ExplainOptions getExplainOptions() { + return explainOptions; + } + + @Nonnull + public RequestOptions getRequestOptions() { + return requestOptions; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (!(o instanceof FirestoreExecutionOptions)) { + return false; + } + FirestoreExecutionOptions that = (FirestoreExecutionOptions) o; + return Objects.equal(explainOptions, that.explainOptions) + && Objects.equal(requestOptions, that.requestOptions); + } + + @Override + public int hashCode() { + return Objects.hashCode(explainOptions, requestOptions); + } + + @Override + public String toString() { + return "FirestoreExecutionOptions{" + + "explainOptions=" + + explainOptions + + ", requestOptions=" + + requestOptions + + "}"; + } + + public Builder toBuilder() { + return new Builder(this); + } + + public static Builder newBuilder() { + return new Builder(); + } + + public static Builder options() { + return new Builder(); + } + + /** Returns a default {@code FirestoreExecutionOptions} instance. */ + public static FirestoreExecutionOptions getDefaultInstance() { + return newBuilder().build(); + } + + /** Builder for {@link FirestoreExecutionOptions}. */ + public static class Builder { + private @Nullable ExplainOptions explainOptions; + private RequestOptions requestOptions = RequestOptions.getDefaultInstance(); + + private Builder() {} + + private Builder(FirestoreExecutionOptions options) { + this.explainOptions = options.explainOptions; + this.requestOptions = options.requestOptions; + } + + public Builder setExplainOptions(@Nullable ExplainOptions explainOptions) { + this.explainOptions = explainOptions; + return this; + } + + public Builder withExplainOptions(@Nullable ExplainOptions explainOptions) { + return setExplainOptions(explainOptions); + } + + public Builder setRequestOptions(@Nonnull RequestOptions requestOptions) { + Preconditions.checkNotNull(requestOptions, "requestOptions cannot be null"); + this.requestOptions = requestOptions; + return this; + } + + public Builder withRequestOptions(@Nonnull RequestOptions requestOptions) { + return setRequestOptions(requestOptions); + } + + @Nonnull + public FirestoreExecutionOptions build() { + return new FirestoreExecutionOptions(this); + } + } +} diff --git a/java-firestore/google-cloud-firestore/src/main/java/com/google/cloud/firestore/FirestoreImpl.java b/java-firestore/google-cloud-firestore/src/main/java/com/google/cloud/firestore/FirestoreImpl.java index 77ed2fa1fbe6..20149f2831f2 100644 --- a/java-firestore/google-cloud-firestore/src/main/java/com/google/cloud/firestore/FirestoreImpl.java +++ b/java-firestore/google-cloud-firestore/src/main/java/com/google/cloud/firestore/FirestoreImpl.java @@ -44,6 +44,7 @@ import com.google.firestore.v1.BatchGetDocumentsRequest; import com.google.firestore.v1.BatchGetDocumentsResponse; import com.google.firestore.v1.DatabaseRootName; +import com.google.firestore.v1.RequestOptions; import com.google.protobuf.ByteString; import java.security.SecureRandom; import java.util.ArrayList; @@ -199,18 +200,45 @@ public Iterable listCollections() { return rootDocument.listCollections(); } + @Nonnull + @Override + public Iterable listCollections( + @Nonnull FirestoreExecutionOptions executionOptions) { + DocumentReference rootDocument = new DocumentReference(this, this.databasePath); + return rootDocument.listCollections(executionOptions); + } + @Nonnull @Override public ApiFuture> getAll( @Nonnull DocumentReference... documentReferences) { - return this.getAll(documentReferences, null, (ByteString) null); + return this.getAll( + documentReferences, null, (ByteString) null, null, (FirestoreExecutionOptions) null); } @Nonnull @Override public ApiFuture> getAll( @Nonnull DocumentReference[] documentReferences, @Nullable FieldMask fieldMask) { - return this.getAll(documentReferences, fieldMask, (ByteString) null); + return this.getAll( + documentReferences, fieldMask, (ByteString) null, null, (FirestoreExecutionOptions) null); + } + + @Nonnull + @Override + public ApiFuture> getAll( + @Nonnull DocumentReference[] documentReferences, + @Nonnull FirestoreExecutionOptions executionOptions) { + return this.getAll(documentReferences, null, (ByteString) null, null, executionOptions); + } + + @Nonnull + @Override + public ApiFuture> getAll( + @Nonnull DocumentReference[] documentReferences, + @Nullable FieldMask fieldMask, + @Nonnull FirestoreExecutionOptions executionOptions) { + return this.getAll(documentReferences, fieldMask, (ByteString) null, null, executionOptions); } @Override @@ -218,7 +246,25 @@ public void getAll( final @Nonnull DocumentReference[] documentReferences, @Nullable FieldMask fieldMask, @Nonnull final ApiStreamObserver apiStreamObserver) { - this.getAll(documentReferences, fieldMask, null, null, apiStreamObserver); + this.getAll(documentReferences, fieldMask, null, null, null, apiStreamObserver); + } + + @Override + public void getAll( + final @Nonnull DocumentReference[] documentReferences, + @Nullable FieldMask fieldMask, + @Nonnull final ApiStreamObserver apiStreamObserver, + @Nonnull FirestoreExecutionOptions executionOptions) { + this.getAll(documentReferences, fieldMask, null, null, executionOptions, apiStreamObserver); + } + + void getAll( + final @Nonnull DocumentReference[] documentReferences, + @Nullable FieldMask fieldMask, + @Nullable ByteString transactionId, + @Nullable com.google.protobuf.Timestamp readTime, + final ApiStreamObserver apiStreamObserver) { + getAll(documentReferences, fieldMask, transactionId, readTime, null, apiStreamObserver); } void getAll( @@ -226,6 +272,7 @@ void getAll( @Nullable FieldMask fieldMask, @Nullable ByteString transactionId, @Nullable com.google.protobuf.Timestamp readTime, + @Nullable FirestoreExecutionOptions executionOptions, final ApiStreamObserver apiStreamObserver) { // To reduce the size of traces, we only register one event for every 100 responses // that we receive from the server. @@ -353,6 +400,12 @@ public void onComplete() { request.addDocuments(docRef.getName()); } + RequestOptions requestOptions = + RequestOptionsHelper.createRequestOptions(getOptions(), executionOptions); + if (!requestOptions.equals(RequestOptions.getDefaultInstance())) { + request.setRequestOptions(requestOptions); + } + streamRequest(request.build(), responseObserver, firestoreClient.batchGetDocumentsCallable()); } @@ -360,14 +413,23 @@ final ApiFuture> getAll( final @Nonnull DocumentReference[] documentReferences, @Nullable FieldMask fieldMask, @Nullable com.google.protobuf.Timestamp readTime) { - return getAll(documentReferences, fieldMask, null, readTime); + return getAll(documentReferences, fieldMask, null, readTime, (FirestoreExecutionOptions) null); + } + + final ApiFuture> getAll( + final @Nonnull DocumentReference[] documentReferences, + @Nullable FieldMask fieldMask, + @Nullable com.google.protobuf.Timestamp readTime, + @Nullable FirestoreExecutionOptions executionOptions) { + return getAll(documentReferences, fieldMask, null, readTime, executionOptions); } private ApiFuture> getAll( final @Nonnull DocumentReference[] documentReferences, @Nullable FieldMask fieldMask, @Nullable ByteString transactionId) { - return getAll(documentReferences, fieldMask, transactionId, null); + return getAll( + documentReferences, fieldMask, transactionId, null, (FirestoreExecutionOptions) null); } /** Internal getAll() method that accepts an optional transaction id. */ @@ -376,6 +438,17 @@ ApiFuture> getAll( @Nullable FieldMask fieldMask, @Nullable ByteString transactionId, @Nullable com.google.protobuf.Timestamp readTime) { + return getAll( + documentReferences, fieldMask, transactionId, readTime, (FirestoreExecutionOptions) null); + } + + /** Internal getAll() method that accepts an optional transaction id and execution options. */ + ApiFuture> getAll( + final @Nonnull DocumentReference[] documentReferences, + @Nullable FieldMask fieldMask, + @Nullable ByteString transactionId, + @Nullable com.google.protobuf.Timestamp readTime, + @Nullable FirestoreExecutionOptions executionOptions) { final SettableApiFuture> futureList = SettableApiFuture.create(); final Map documentSnapshotMap = new HashMap<>(); getAll( @@ -383,6 +456,7 @@ ApiFuture> getAll( fieldMask, transactionId, readTime, + executionOptions, new ApiStreamObserver() { @Override public void onNext(DocumentSnapshot documentSnapshot) { @@ -426,7 +500,18 @@ public PipelineSource pipeline() { @Override public ApiFuture runTransaction(@Nonnull final Transaction.Function updateFunction) { return runAsyncTransaction( - new TransactionAsyncAdapter<>(updateFunction), TransactionOptions.create()); + new TransactionAsyncAdapter<>(updateFunction), TransactionOptions.create(), null); + } + + @Nonnull + @Override + public ApiFuture runTransaction( + @Nonnull final Transaction.Function updateFunction, + @Nonnull FirestoreExecutionOptions executionOptions) { + return runAsyncTransaction( + new TransactionAsyncAdapter<>(updateFunction), + TransactionOptions.create(), + executionOptions); } @Nonnull @@ -434,14 +519,33 @@ public ApiFuture runTransaction(@Nonnull final Transaction.Function up public ApiFuture runTransaction( @Nonnull final Transaction.Function updateFunction, @Nonnull TransactionOptions transactionOptions) { - return runAsyncTransaction(new TransactionAsyncAdapter<>(updateFunction), transactionOptions); + return runAsyncTransaction( + new TransactionAsyncAdapter<>(updateFunction), transactionOptions, null); + } + + @Nonnull + @Override + public ApiFuture runTransaction( + @Nonnull final Transaction.Function updateFunction, + @Nonnull TransactionOptions transactionOptions, + @Nonnull FirestoreExecutionOptions executionOptions) { + return runAsyncTransaction( + new TransactionAsyncAdapter<>(updateFunction), transactionOptions, executionOptions); } @Nonnull @Override public ApiFuture runAsyncTransaction( @Nonnull final Transaction.AsyncFunction updateFunction) { - return runAsyncTransaction(updateFunction, TransactionOptions.create()); + return runAsyncTransaction(updateFunction, TransactionOptions.create(), null); + } + + @Nonnull + @Override + public ApiFuture runAsyncTransaction( + @Nonnull final Transaction.AsyncFunction updateFunction, + @Nonnull FirestoreExecutionOptions executionOptions) { + return runAsyncTransaction(updateFunction, TransactionOptions.create(), executionOptions); } @Nonnull @@ -449,6 +553,15 @@ public ApiFuture runAsyncTransaction( public ApiFuture runAsyncTransaction( @Nonnull final Transaction.AsyncFunction updateFunction, @Nonnull TransactionOptions transactionOptions) { + return runAsyncTransaction(updateFunction, transactionOptions, null); + } + + @Nonnull + @Override + public ApiFuture runAsyncTransaction( + @Nonnull final Transaction.AsyncFunction updateFunction, + @Nonnull TransactionOptions transactionOptions, + @Nullable FirestoreExecutionOptions executionOptions) { MetricsContext metricsContext = getOptions() @@ -466,7 +579,10 @@ public ApiFuture runAsyncTransaction( } else { // For READ_ONLY transactions without readTime, there is still strong consistency applied, // that cannot be tracked client side. - result = new ServerSideTransactionRunner<>(this, updateFunction, transactionOptions).run(); + result = + new ServerSideTransactionRunner<>( + this, updateFunction, transactionOptions, executionOptions) + .run(); } metricsContext.recordLatencyAtFuture(MetricType.END_TO_END_LATENCY, result); } catch (Exception error) { diff --git a/java-firestore/google-cloud-firestore/src/main/java/com/google/cloud/firestore/FirestoreOptions.java b/java-firestore/google-cloud-firestore/src/main/java/com/google/cloud/firestore/FirestoreOptions.java index 00baa6a3d9f3..8eff690d74b6 100644 --- a/java-firestore/google-cloud-firestore/src/main/java/com/google/cloud/firestore/FirestoreOptions.java +++ b/java-firestore/google-cloud-firestore/src/main/java/com/google/cloud/firestore/FirestoreOptions.java @@ -17,6 +17,7 @@ package com.google.cloud.firestore; import com.google.api.core.ApiFunction; +import com.google.api.core.BetaApi; import com.google.api.core.InternalApi; import com.google.api.gax.core.CredentialsProvider; import com.google.api.gax.core.FixedCredentialsProvider; @@ -34,6 +35,8 @@ import com.google.cloud.firestore.telemetry.MetricsUtil; import com.google.cloud.firestore.v1.FirestoreSettings; import com.google.cloud.grpc.GrpcTransportOptions; +import com.google.common.base.Preconditions; +import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; import com.google.common.collect.ImmutableSet; import io.grpc.ManagedChannelBuilder; @@ -69,6 +72,7 @@ public final class FirestoreOptions extends ServiceOptions requestTags; public static class DefaultFirestoreFactory implements FirestoreFactory { @@ -158,6 +162,13 @@ com.google.cloud.firestore.telemetry.MetricsUtil getMetricsUtil() { return metricsUtil; } + /** Returns the list of request tags. */ + @BetaApi + @Nonnull + public List getRequestTags() { + return requestTags; + } + @Nonnull public FirestoreOpenTelemetryOptions getOpenTelemetryOptions() { return openTelemetryOptions; @@ -171,6 +182,7 @@ public static class Builder extends ServiceOptions.Builder requestTags = null; private Builder() {} @@ -182,6 +194,7 @@ private Builder(FirestoreOptions options) { this.emulatorHost = options.emulatorHost; this.alwaysUseImplicitOrderBy = options.alwaysUseImplicitOrderBy; this.openTelemetryOptions = options.openTelemetryOptions; + this.requestTags = options.requestTags; } /** @@ -270,12 +283,25 @@ public Builder setDatabaseId(@Nonnull String databaseId) { return this; } + /** + * Sets the request tags for the client. + * + * @param requestTags the request tags to set + * @return this builder + */ + @BetaApi + @Nonnull + public Builder setRequestTags(@Nonnull List requestTags) { + Preconditions.checkNotNull(requestTags, "requestTags cannot be null"); + this.requestTags = ImmutableList.copyOf(requestTags); + return this; + } + /** * Sets the {@link FirestoreOpenTelemetryOptions} to be used for this Firestore instance. * * @param openTelemetryOptions The `FirestoreOpenTelemetryOptions` to use. */ - @Nonnull public Builder setOpenTelemetryOptions( @Nonnull FirestoreOpenTelemetryOptions openTelemetryOptions) { this.openTelemetryOptions = openTelemetryOptions; @@ -378,6 +404,10 @@ protected FirestoreOptions(Builder builder) { // Set up the `MetricsUtil` instance after the database ID has been set. this.metricsUtil = MetricsUtil.getInstance(this); + this.requestTags = + builder.requestTags != null + ? ImmutableList.copyOf(builder.requestTags) + : ImmutableList.of(); if (builder.channelProvider == null) { ApiFunction channelConfigurator = @@ -479,12 +509,13 @@ public boolean equals(Object o) { FirestoreOptions that = (FirestoreOptions) o; return Objects.equals(databaseId, that.databaseId) && Objects.equals(channelProvider, that.channelProvider) + && Objects.equals(requestTags, that.requestTags) && baseEquals(that); } @Override public int hashCode() { - return Objects.hash(databaseId, channelProvider, baseHashCode()); + return Objects.hash(databaseId, channelProvider, requestTags, baseHashCode()); } @Nonnull diff --git a/java-firestore/google-cloud-firestore/src/main/java/com/google/cloud/firestore/Pipeline.java b/java-firestore/google-cloud-firestore/src/main/java/com/google/cloud/firestore/Pipeline.java index c49ee0ad2f9a..d633a9405d94 100644 --- a/java-firestore/google-cloud-firestore/src/main/java/com/google/cloud/firestore/Pipeline.java +++ b/java-firestore/google-cloud-firestore/src/main/java/com/google/cloud/firestore/Pipeline.java @@ -75,6 +75,7 @@ import com.google.firestore.v1.Document; import com.google.firestore.v1.ExecutePipelineRequest; import com.google.firestore.v1.ExecutePipelineResponse; +import com.google.firestore.v1.RequestOptions; import com.google.firestore.v1.StructuredPipeline; import com.google.firestore.v1.Value; import com.google.protobuf.ByteString; @@ -1364,11 +1365,35 @@ public Pipeline rawStage(RawStage stage) { * @return An {@link ApiFuture} representing the asynchronous pipeline execution. */ public ApiFuture execute() { - return execute(new PipelineExecuteOptions(), null, null); + return execute(new PipelineExecuteOptions(), null, null, null); + } + + /** + * Executes this pipeline with execution options. + * + * @param executionOptions Options for executing the request. + * @return An {@link ApiFuture} representing the asynchronous pipeline execution. + */ + @BetaApi + public ApiFuture execute(@Nonnull FirestoreExecutionOptions executionOptions) { + return execute(new PipelineExecuteOptions(), null, null, executionOptions); } public ApiFuture execute(PipelineExecuteOptions options) { - return execute(options, null, null); + return execute(options, null, null, null); + } + + /** + * Executes this pipeline with pipeline execute options and execution options. + * + * @param options The options for pipeline execution. + * @param executionOptions Options for executing the request. + * @return An {@link ApiFuture} representing the asynchronous pipeline execution. + */ + @BetaApi + public ApiFuture execute( + PipelineExecuteOptions options, @Nullable FirestoreExecutionOptions executionOptions) { + return execute(options, null, null, executionOptions); } MetricsContext createMetricsContext(String methodName) { @@ -1421,6 +1446,36 @@ MetricsContext createMetricsContext(String methodName) { * @param observer The {@link ApiStreamObserver} to receive pipeline results and events. */ public void execute(ApiStreamObserver observer) { + execute(observer, null); + } + + /** + * Executes this pipeline with execution options, providing results to the given {@link + * ApiStreamObserver} as they become available. + * + * @param observer The {@link ApiStreamObserver} to receive pipeline results and events. + * @param executionOptions Options for executing the request. + */ + @BetaApi + public void execute( + ApiStreamObserver observer, + @Nullable FirestoreExecutionOptions executionOptions) { + execute(new PipelineExecuteOptions(), observer, executionOptions); + } + + /** + * Executes this pipeline with options and execution options, providing results to the given + * {@link ApiStreamObserver} as they become available. + * + * @param options Options for pipeline execution. + * @param observer The {@link ApiStreamObserver} to receive pipeline results and events. + * @param executionOptions Options for executing the request. + */ + @BetaApi + public void execute( + PipelineExecuteOptions options, + ApiStreamObserver observer, + @Nullable FirestoreExecutionOptions executionOptions) { if (this.rpcContext == null) { throw new IllegalStateException( "This pipeline was created without a database (e.g., as a subcollection pipeline) and" @@ -1430,9 +1485,10 @@ public void execute(ApiStreamObserver observer) { createMetricsContext(TelemetryConstants.METHOD_NAME_EXECUTE_PIPELINE_EXECUTE); executeInternal( - new PipelineExecuteOptions(), + options, null, null, + executionOptions, new PipelineResultObserver() { @Override public void onNext(PipelineResult result) { @@ -1456,6 +1512,14 @@ ApiFuture execute( @Nonnull PipelineExecuteOptions options, @Nullable final ByteString transactionId, @Nullable com.google.protobuf.Timestamp readTime) { + return execute(options, transactionId, readTime, null); + } + + ApiFuture execute( + @Nonnull PipelineExecuteOptions options, + @Nullable final ByteString transactionId, + @Nullable com.google.protobuf.Timestamp readTime, + @Nullable FirestoreExecutionOptions executionOptions) { if (this.rpcContext == null) { throw new IllegalStateException( "This pipeline was created without a database (e.g., as a subcollection pipeline) and" @@ -1479,6 +1543,7 @@ ApiFuture execute( options, transactionId, readTime, + executionOptions, new PipelineResultObserver() { final List results = new ArrayList<>(); @@ -1513,6 +1578,7 @@ void executeInternal( @Nonnull PipelineExecuteOptions options, @Nullable final ByteString transactionId, @Nullable com.google.protobuf.Timestamp readTime, + @Nullable FirestoreExecutionOptions executionOptions, PipelineResultObserver observer, MetricsContext metricsContext) { ExecutePipelineRequest.Builder request = @@ -1532,6 +1598,13 @@ void executeInternal( request.setReadTime(readTime); } + RequestOptions requestOptions = + RequestOptionsHelper.createRequestOptions( + rpcContext.getFirestore().getOptions(), executionOptions); + if (!requestOptions.equals(RequestOptions.getDefaultInstance())) { + request.setRequestOptions(requestOptions); + } + pipelineInternalStream( request.build(), new PipelineResultObserver() { diff --git a/java-firestore/google-cloud-firestore/src/main/java/com/google/cloud/firestore/Query.java b/java-firestore/google-cloud-firestore/src/main/java/com/google/cloud/firestore/Query.java index ec90cbe9d369..1385d4abab19 100644 --- a/java-firestore/google-cloud-firestore/src/main/java/com/google/cloud/firestore/Query.java +++ b/java-firestore/google-cloud-firestore/src/main/java/com/google/cloud/firestore/Query.java @@ -32,6 +32,7 @@ import static com.google.firestore.v1.StructuredQuery.FieldFilter.Operator.NOT_IN; import com.google.api.core.ApiFuture; +import com.google.api.core.BetaApi; import com.google.api.core.InternalExtensionOnly; import com.google.api.core.SettableApiFuture; import com.google.api.gax.rpc.ApiStreamObserver; @@ -1533,6 +1534,20 @@ private StructuredQuery.Builder buildWithoutClientTranslation(boolean forceImpli * @param responseObserver The observer to be notified when results arrive. */ public void stream(@Nonnull final ApiStreamObserver responseObserver) { + stream(responseObserver, null); + } + + /** + * Executes the query and streams the results as a StreamObserver of DocumentSnapshots, with + * execution options. + * + * @param responseObserver The observer to be notified when results arrive. + * @param executionOptions Options for executing the request. + */ + @BetaApi + public void stream( + @Nonnull final ApiStreamObserver responseObserver, + @Nullable FirestoreExecutionOptions executionOptions) { Preconditions.checkState( !LimitType.Last.equals(Query.this.options.getLimitType()), "Query results for queries that include limitToLast() constraints cannot be streamed. " @@ -1571,6 +1586,7 @@ public void onCompleted() { /* transactionId= */ null, /* readTime= */ null, /* explainOptions= */ null, + executionOptions, /* isRetryRequestWithCursor= */ false); } @@ -1650,7 +1666,23 @@ public void onCompleted() { * @return the serialized RunQueryRequest */ public RunQueryRequest toProto() { - return toRunQueryRequestBuilder(null, null, null).build(); + return toProto(null, null, null, null); + } + + RunQueryRequest toProto( + @Nullable final ByteString transactionId, + @Nullable final Timestamp readTime, + @Nullable ExplainOptions explainOptions) { + return toProto(transactionId, readTime, explainOptions, null); + } + + RunQueryRequest toProto( + @Nullable final ByteString transactionId, + @Nullable final Timestamp readTime, + @Nullable ExplainOptions explainOptions, + @Nullable FirestoreExecutionOptions executionOptions) { + return toRunQueryRequestBuilder(transactionId, readTime, explainOptions, executionOptions) + .build(); } @Override @@ -1783,7 +1815,19 @@ private Value encodeValue(FieldPath fieldPath, Object value) { @Override @Nonnull public ApiFuture get() { - return get(null, null); + return get(null, null, null, null); + } + + /** + * Executes the query with execution options and returns the results as QuerySnapshot. + * + * @param executionOptions Options for executing the request. + * @return An ApiFuture that will be resolved with the results of the Query. + */ + @BetaApi + @Nonnull + public ApiFuture get(@Nonnull FirestoreExecutionOptions executionOptions) { + return get(null, null, executionOptions.getExplainOptions(), executionOptions); } /** @@ -1797,7 +1841,36 @@ public ApiFuture get() { @Override @Nonnull public ApiFuture> explain(ExplainOptions options) { - return super.explain(options); + return super.explain(options, null); + } + + /** + * Plans and optionally executes this query with execution options. + * + * @param executionOptions Options for executing the request. + * @return An ApiFuture that will be resolved with the planner information, statistics from the + * query execution (if any), and the query results (if any). + */ + @BetaApi + @Nonnull + public ApiFuture> explain( + @Nonnull FirestoreExecutionOptions executionOptions) { + return super.explain(executionOptions); + } + + /** + * Plans and optionally executes this query with explain options and execution options. + * + * @param options The options for explain. + * @param executionOptions Options for executing the request. + * @return An ApiFuture that will be resolved with the planner information, statistics from the + * query execution (if any), and the query results (if any). + */ + @BetaApi + @Nonnull + public ApiFuture> explain( + @Nonnull ExplainOptions options, @Nonnull FirestoreExecutionOptions executionOptions) { + return super.explain(options, executionOptions); } /** diff --git a/java-firestore/google-cloud-firestore/src/main/java/com/google/cloud/firestore/ReadTimeTransaction.java b/java-firestore/google-cloud-firestore/src/main/java/com/google/cloud/firestore/ReadTimeTransaction.java index 4c30caa4d14e..c66993657b92 100644 --- a/java-firestore/google-cloud-firestore/src/main/java/com/google/cloud/firestore/ReadTimeTransaction.java +++ b/java-firestore/google-cloud-firestore/src/main/java/com/google/cloud/firestore/ReadTimeTransaction.java @@ -57,6 +57,14 @@ public boolean hasTransactionId() { @Nonnull @Override public ApiFuture get(@Nonnull DocumentReference documentRef) { + return get(documentRef, (FirestoreExecutionOptions) null); + } + + @Nonnull + @Override + public ApiFuture get( + @Nonnull DocumentReference documentRef, + @Nullable FirestoreExecutionOptions executionOptions) { TraceUtil.Span span = getTraceUtil() .startSpan( @@ -65,7 +73,10 @@ public ApiFuture get(@Nonnull DocumentReference documentRef) { ApiFuture result = ApiFutures.transform( firestore.getAll( - new DocumentReference[] {documentRef}, /* fieldMask= */ null, readTime), + new DocumentReference[] {documentRef}, + /* fieldMask= */ null, + readTime, + executionOptions), snapshots -> snapshots.isEmpty() ? null : snapshots.get(0), MoreExecutors.directExecutor()); span.endAtFuture(result); @@ -80,13 +91,21 @@ public ApiFuture get(@Nonnull DocumentReference documentRef) { @Override public ApiFuture> getAll( @Nonnull DocumentReference... documentReferences) { + return getAll(documentReferences, (FirestoreExecutionOptions) null); + } + + @Nonnull + @Override + public ApiFuture> getAll( + @Nonnull DocumentReference[] documentReferences, + @Nullable FirestoreExecutionOptions executionOptions) { TraceUtil.Span span = getTraceUtil() .startSpan( TelemetryConstants.METHOD_NAME_TRANSACTION_GET_DOCUMENTS, transactionTraceContext); try (TraceUtil.Scope ignored = span.makeCurrent()) { ApiFuture> result = - firestore.getAll(documentReferences, /* fieldMask= */ null, readTime); + firestore.getAll(documentReferences, /* fieldMask= */ null, readTime, executionOptions); span.endAtFuture(result); return result; } catch (Exception error) { @@ -99,13 +118,22 @@ public ApiFuture> getAll( @Override public ApiFuture> getAll( @Nonnull DocumentReference[] documentReferences, @Nullable FieldMask fieldMask) { + return getAll(documentReferences, fieldMask, (FirestoreExecutionOptions) null); + } + + @Nonnull + @Override + public ApiFuture> getAll( + @Nonnull DocumentReference[] documentReferences, + @Nullable FieldMask fieldMask, + @Nullable FirestoreExecutionOptions executionOptions) { TraceUtil.Span span = getTraceUtil() .startSpan( TelemetryConstants.METHOD_NAME_TRANSACTION_GET_DOCUMENTS, transactionTraceContext); try (TraceUtil.Scope ignored = span.makeCurrent()) { ApiFuture> result = - firestore.getAll(documentReferences, /* fieldMask= */ null, readTime); + firestore.getAll(documentReferences, fieldMask, readTime, executionOptions); span.endAtFuture(result); return result; } catch (Exception error) { @@ -117,31 +145,62 @@ public ApiFuture> getAll( @Nonnull @Override public ApiFuture get(@Nonnull Query query) { + return get(query, (FirestoreExecutionOptions) null); + } + + @Nonnull + @Override + public ApiFuture get( + @Nonnull Query query, @Nullable FirestoreExecutionOptions executionOptions) { try (TraceUtil.Scope ignored = transactionTraceContext.makeCurrent()) { - return query.get(null, com.google.cloud.Timestamp.fromProto(readTime)); + return query.get( + null, com.google.cloud.Timestamp.fromProto(readTime), null, executionOptions); } } @Nonnull @Override public ApiFuture get(@Nonnull AggregateQuery query) { + return get(query, (FirestoreExecutionOptions) null); + } + + @Nonnull + @Override + public ApiFuture get( + @Nonnull AggregateQuery query, @Nullable FirestoreExecutionOptions executionOptions) { try (TraceUtil.Scope ignored = transactionTraceContext.makeCurrent()) { - return query.get(null, readTime); + return query.get(null, readTime, null, executionOptions); } } @Nonnull @Override public ApiFuture execute(@Nonnull Pipeline pipeline) { - return execute(pipeline, new PipelineExecuteOptions()); + return execute(pipeline, new PipelineExecuteOptions(), null); + } + + @Nonnull + @Override + public ApiFuture execute( + @Nonnull Pipeline pipeline, @Nullable FirestoreExecutionOptions executionOptions) { + return execute(pipeline, new PipelineExecuteOptions(), executionOptions); } @Nonnull @Override public ApiFuture execute( @Nonnull Pipeline pipeline, @Nonnull PipelineExecuteOptions options) { + return execute(pipeline, options, null); + } + + @Nonnull + @Override + public ApiFuture execute( + @Nonnull Pipeline pipeline, + @Nonnull PipelineExecuteOptions options, + @Nullable FirestoreExecutionOptions executionOptions) { try (TraceUtil.Scope ignored = transactionTraceContext.makeCurrent()) { - return pipeline.execute(options, null, readTime); + return pipeline.execute(options, null, readTime, executionOptions); } } @@ -201,7 +260,7 @@ public Transaction update( public Transaction update( @Nonnull DocumentReference documentReference, @Nonnull Map fields, - Precondition precondition) { + Precondition options) { throw new IllegalStateException(WRITE_EXCEPTION_MSG); } diff --git a/java-firestore/google-cloud-firestore/src/main/java/com/google/cloud/firestore/RequestOptionsHelper.java b/java-firestore/google-cloud-firestore/src/main/java/com/google/cloud/firestore/RequestOptionsHelper.java new file mode 100644 index 000000000000..4bb9378152c3 --- /dev/null +++ b/java-firestore/google-cloud-firestore/src/main/java/com/google/cloud/firestore/RequestOptionsHelper.java @@ -0,0 +1,75 @@ +/* + * Copyright 2026 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.cloud.firestore; + +import com.google.api.core.InternalApi; +import com.google.firestore.v1.RequestOptions; +import java.util.List; +import javax.annotation.Nullable; + +/** Helper class for building and merging Firestore request options. */ +@InternalApi +public final class RequestOptionsHelper { + + private RequestOptionsHelper() {} + + /** + * Creates a merged {@link RequestOptions} proto instance combining request-level options from + * {@link FirestoreExecutionOptions} with instance-level tags. + */ + public static RequestOptions createRequestOptions( + @Nullable FirestoreOptions firestoreOptions, + @Nullable FirestoreExecutionOptions executionOptions) { + return createRequestOptions( + firestoreOptions, executionOptions != null ? executionOptions.getRequestOptions() : null); + } + + /** + * Creates a merged {@link RequestOptions} proto instance combining model {@link + * com.google.cloud.firestore.models.RequestOptions} with instance-level tags. + */ + public static RequestOptions createRequestOptions( + @Nullable FirestoreOptions firestoreOptions, + @Nullable com.google.cloud.firestore.models.RequestOptions requestOptions) { + return createRequestOptions( + firestoreOptions, requestOptions != null ? requestOptions.toPb() : null); + } + + /** + * Creates a merged {@link RequestOptions} proto instance combining request-level proto options + * with instance-level tags. + */ + public static RequestOptions createRequestOptions( + @Nullable FirestoreOptions firestoreOptions, @Nullable RequestOptions requestOptions) { + List instanceTags = firestoreOptions != null ? firestoreOptions.getRequestTags() : null; + boolean hasInstanceTags = instanceTags != null && !instanceTags.isEmpty(); + boolean hasRequestOptions = requestOptions != null && requestOptions.getRequestTagsCount() > 0; + + if (!hasInstanceTags && !hasRequestOptions) { + return requestOptions != null ? requestOptions : RequestOptions.getDefaultInstance(); + } + + RequestOptions.Builder builder = RequestOptions.newBuilder(); + if (requestOptions != null) { + builder.mergeFrom(requestOptions); + } + if (hasInstanceTags) { + builder.addAllRequestTags(instanceTags); + } + return builder.build(); + } +} diff --git a/java-firestore/google-cloud-firestore/src/main/java/com/google/cloud/firestore/ServerSideTransaction.java b/java-firestore/google-cloud-firestore/src/main/java/com/google/cloud/firestore/ServerSideTransaction.java index 1ad6adadec14..2ef1f9d87cb0 100644 --- a/java-firestore/google-cloud-firestore/src/main/java/com/google/cloud/firestore/ServerSideTransaction.java +++ b/java-firestore/google-cloud-firestore/src/main/java/com/google/cloud/firestore/ServerSideTransaction.java @@ -26,6 +26,7 @@ import com.google.common.util.concurrent.MoreExecutors; import com.google.firestore.v1.BeginTransactionRequest; import com.google.firestore.v1.BeginTransactionResponse; +import com.google.firestore.v1.RequestOptions; import com.google.firestore.v1.RollbackRequest; import com.google.firestore.v1.TransactionOptions.ReadOnly; import com.google.protobuf.ByteString; @@ -70,6 +71,14 @@ public static ApiFuture begin( FirestoreImpl firestore, TransactionOptions transactionOptions, @Nullable ServerSideTransaction previousTransaction) { + return begin(firestore, transactionOptions, previousTransaction, null); + } + + public static ApiFuture begin( + FirestoreImpl firestore, + TransactionOptions transactionOptions, + @Nullable ServerSideTransaction previousTransaction, + @Nullable FirestoreExecutionOptions executionOptions) { BeginTransactionRequest.Builder beginTransaction = BeginTransactionRequest.newBuilder(); beginTransaction.setDatabase(firestore.getDatabaseName()); ByteString previousTransactionId = @@ -89,6 +98,12 @@ public static ApiFuture begin( beginTransaction.getOptionsBuilder().setReadOnly(readOnlyBuilder); } + RequestOptions requestOptions = + RequestOptionsHelper.createRequestOptions(firestore.getOptions(), executionOptions); + if (!requestOptions.equals(RequestOptions.getDefaultInstance())) { + beginTransaction.setRequestOptions(requestOptions); + } + ApiFuture transactionBeginFuture = firestore.sendRequest( beginTransaction.build(), firestore.getClient().beginTransactionCallable()); @@ -102,26 +117,40 @@ public static ApiFuture begin( /** Commits a transaction. */ ApiFuture> commit() { + return commit((FirestoreExecutionOptions) null); + } + + /** Commits a transaction with execution options. */ + ApiFuture> commit(@Nullable FirestoreExecutionOptions executionOptions) { try (TraceUtil.Scope ignored = transactionTraceContext.makeCurrent()) { - return super.commit(transactionId); + return super.commit(transactionId, executionOptions); } } /** Rolls a transaction back and releases all read locks. */ ApiFuture rollback() { + return rollback((FirestoreExecutionOptions) null); + } + + /** Rolls a transaction back with execution options and releases all read locks. */ + ApiFuture rollback(@Nullable FirestoreExecutionOptions executionOptions) { TraceUtil.Span span = getTraceUtil() .startSpan( TelemetryConstants.METHOD_NAME_TRANSACTION_ROLLBACK, transactionTraceContext); try (TraceUtil.Scope ignored = span.makeCurrent()) { - RollbackRequest req = + RollbackRequest.Builder req = RollbackRequest.newBuilder() .setTransaction(transactionId) - .setDatabase(firestore.getDatabaseName()) - .build(); + .setDatabase(firestore.getDatabaseName()); + RequestOptions requestOptions = + RequestOptionsHelper.createRequestOptions(firestore.getOptions(), executionOptions); + if (!requestOptions.equals(RequestOptions.getDefaultInstance())) { + req.setRequestOptions(requestOptions); + } ApiFuture rollbackFuture = - firestore.sendRequest(req, firestore.getClient().rollbackCallable()); + firestore.sendRequest(req.build(), firestore.getClient().rollbackCallable()); ApiFuture transform = ApiFutures.transform(rollbackFuture, resp -> null, MoreExecutors.directExecutor()); @@ -160,6 +189,14 @@ public boolean hasTransactionId() { @Override @Nonnull public ApiFuture get(@Nonnull DocumentReference documentRef) { + return get(documentRef, (FirestoreExecutionOptions) null); + } + + @Override + @Nonnull + public ApiFuture get( + @Nonnull DocumentReference documentRef, + @Nullable FirestoreExecutionOptions executionOptions) { TraceUtil.Span span = getTraceUtil() .startSpan( @@ -172,7 +209,8 @@ public ApiFuture get(@Nonnull DocumentReference documentRef) { new DocumentReference[] {documentRef}, /* fieldMask= */ null, transactionId, - /* readTime= */ null), + /* readTime= */ null, + executionOptions), snapshots -> snapshots.isEmpty() ? null : snapshots.get(0), MoreExecutors.directExecutor()); span.endAtFuture(result); @@ -193,6 +231,14 @@ public ApiFuture get(@Nonnull DocumentReference documentRef) { @Nonnull public ApiFuture> getAll( @Nonnull DocumentReference... documentReferences) { + return getAll(documentReferences, (FirestoreExecutionOptions) null); + } + + @Override + @Nonnull + public ApiFuture> getAll( + @Nonnull DocumentReference[] documentReferences, + @Nullable FirestoreExecutionOptions executionOptions) { Preconditions.checkState(isEmpty(), READ_BEFORE_WRITE_ERROR_MSG); TraceUtil.Span span = getTraceUtil() @@ -201,7 +247,11 @@ public ApiFuture> getAll( try (TraceUtil.Scope ignored = span.makeCurrent()) { ApiFuture> result = firestore.getAll( - documentReferences, /* fieldMask= */ null, transactionId, /* readTime= */ null); + documentReferences, + /* fieldMask= */ null, + transactionId, + /* readTime= */ null, + executionOptions); span.endAtFuture(result); return result; } catch (Exception error) { @@ -222,6 +272,15 @@ public ApiFuture> getAll( @Nonnull public ApiFuture> getAll( @Nonnull DocumentReference[] documentReferences, @Nullable FieldMask fieldMask) { + return getAll(documentReferences, fieldMask, (FirestoreExecutionOptions) null); + } + + @Override + @Nonnull + public ApiFuture> getAll( + @Nonnull DocumentReference[] documentReferences, + @Nullable FieldMask fieldMask, + @Nullable FirestoreExecutionOptions executionOptions) { Preconditions.checkState(isEmpty(), READ_BEFORE_WRITE_ERROR_MSG); TraceUtil.Span span = getTraceUtil() @@ -229,7 +288,8 @@ public ApiFuture> getAll( TelemetryConstants.METHOD_NAME_TRANSACTION_GET_DOCUMENTS, transactionTraceContext); try (TraceUtil.Scope ignored = span.makeCurrent()) { ApiFuture> result = - firestore.getAll(documentReferences, fieldMask, transactionId, /* readTime= */ null); + firestore.getAll( + documentReferences, fieldMask, transactionId, /* readTime= */ null, executionOptions); span.endAtFuture(result); return result; } catch (Exception error) { @@ -247,9 +307,17 @@ public ApiFuture> getAll( @Override @Nonnull public ApiFuture get(@Nonnull Query query) { + return get(query, (FirestoreExecutionOptions) null); + } + + @Override + @Nonnull + public ApiFuture get( + @Nonnull Query query, @Nullable FirestoreExecutionOptions executionOptions) { Preconditions.checkState(isEmpty(), READ_BEFORE_WRITE_ERROR_MSG); try (TraceUtil.Scope ignored = transactionTraceContext.makeCurrent()) { - return query.get(transactionId, /* readTime= */ null); + return query.get( + transactionId, /* readTime= */ null, /* explainOptions= */ null, executionOptions); } } @@ -262,26 +330,48 @@ public ApiFuture get(@Nonnull Query query) { @Override @Nonnull public ApiFuture get(@Nonnull AggregateQuery query) { + return get(query, (FirestoreExecutionOptions) null); + } + + @Override + @Nonnull + public ApiFuture get( + @Nonnull AggregateQuery query, @Nullable FirestoreExecutionOptions executionOptions) { Preconditions.checkState(isEmpty(), READ_BEFORE_WRITE_ERROR_MSG); try (TraceUtil.Scope ignored = transactionTraceContext.makeCurrent()) { - return query.get(transactionId, null); + return query.get(transactionId, null, null, executionOptions); } } @Nonnull @Override public ApiFuture execute(@Nonnull Pipeline pipeline) { - Preconditions.checkState(isEmpty(), READ_BEFORE_WRITE_ERROR_MSG); - return execute(pipeline, new PipelineExecuteOptions()); + return execute(pipeline, new PipelineExecuteOptions(), null); + } + + @Nonnull + @Override + public ApiFuture execute( + @Nonnull Pipeline pipeline, @Nullable FirestoreExecutionOptions executionOptions) { + return execute(pipeline, new PipelineExecuteOptions(), executionOptions); } @Nonnull @Override public ApiFuture execute( @Nonnull Pipeline pipeline, @Nonnull PipelineExecuteOptions options) { + return execute(pipeline, options, null); + } + + @Nonnull + @Override + public ApiFuture execute( + @Nonnull Pipeline pipeline, + @Nonnull PipelineExecuteOptions options, + @Nullable FirestoreExecutionOptions executionOptions) { Preconditions.checkState(isEmpty(), READ_BEFORE_WRITE_ERROR_MSG); try (TraceUtil.Scope ignored = transactionTraceContext.makeCurrent()) { - return pipeline.execute(options, transactionId, null); + return pipeline.execute(options, transactionId, null, executionOptions); } } } diff --git a/java-firestore/google-cloud-firestore/src/main/java/com/google/cloud/firestore/ServerSideTransactionRunner.java b/java-firestore/google-cloud-firestore/src/main/java/com/google/cloud/firestore/ServerSideTransactionRunner.java index 839d549a595b..e00bdeedb6e9 100644 --- a/java-firestore/google-cloud-firestore/src/main/java/com/google/cloud/firestore/ServerSideTransactionRunner.java +++ b/java-firestore/google-cloud-firestore/src/main/java/com/google/cloud/firestore/ServerSideTransactionRunner.java @@ -38,6 +38,7 @@ import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.TimeUnit; import javax.annotation.Nonnull; +import javax.annotation.Nullable; /** * Implements backoff and retry semantics for Firestore transactions. @@ -57,6 +58,7 @@ final class ServerSideTransactionRunner { private final Executor userCallbackExecutor; private final ExponentialRetryAlgorithm backoffAlgorithm; private final TransactionOptions transactionOptions; + @Nullable private final FirestoreExecutionOptions executionOptions; private TimedAttemptSettings nextBackoffAttempt; private ServerSideTransaction transaction; private int attemptsRemaining; @@ -74,7 +76,16 @@ final class ServerSideTransactionRunner { FirestoreImpl firestore, Transaction.AsyncFunction userCallback, TransactionOptions transactionOptions) { + this(firestore, userCallback, transactionOptions, null); + } + + ServerSideTransactionRunner( + FirestoreImpl firestore, + Transaction.AsyncFunction userCallback, + TransactionOptions transactionOptions, + @Nullable FirestoreExecutionOptions executionOptions) { this.transactionOptions = transactionOptions; + this.executionOptions = executionOptions; this.firestore = firestore; this.firestoreExecutor = firestore.getClient().getExecutor(); this.userCallback = userCallback; @@ -142,7 +153,8 @@ ApiFuture begin() { ServerSideTransaction previousTransaction = this.transaction; this.transaction = null; ApiFuture result = - ServerSideTransaction.begin(firestore, transactionOptions, previousTransaction); + ServerSideTransaction.begin( + firestore, transactionOptions, previousTransaction, executionOptions); result = ApiFutures.transform( result, @@ -159,7 +171,9 @@ ApiFuture begin() { } private ApiFuture maybeRollback() { - return hasTransaction() ? transaction.rollback() : ApiFutures.immediateFuture(null); + return hasTransaction() + ? transaction.rollback(executionOptions) + : ApiFutures.immediateFuture(null); } private boolean hasTransaction() { @@ -234,7 +248,7 @@ private ApiFuture beginTransactionCallback(ServerSideTransaction serverSideTr */ private ApiFuture userFunctionCallback(T userFunctionResult) { return ApiFutures.transform( - transaction.commit(), + transaction.commit(executionOptions), // The callback that is invoked after the Commit RPC returns. It returns the user result. input -> userFunctionResult, MoreExecutors.directExecutor()); @@ -299,7 +313,7 @@ private ApiFuture rollbackAndReject(final Throwable throwable) { // We use `addListener()` since we want to return the original exception regardless of // whether rollback() succeeds. transaction - .rollback() + .rollback(executionOptions) .addListener( () -> { runTransactionSpan.end(throwable); diff --git a/java-firestore/google-cloud-firestore/src/main/java/com/google/cloud/firestore/StreamableQuery.java b/java-firestore/google-cloud-firestore/src/main/java/com/google/cloud/firestore/StreamableQuery.java index 6e331af22ebe..848a4bd796d8 100644 --- a/java-firestore/google-cloud-firestore/src/main/java/com/google/cloud/firestore/StreamableQuery.java +++ b/java-firestore/google-cloud-firestore/src/main/java/com/google/cloud/firestore/StreamableQuery.java @@ -35,6 +35,7 @@ import com.google.cloud.firestore.v1.FirestoreSettings; import com.google.common.collect.ImmutableMap; import com.google.firestore.v1.Document; +import com.google.firestore.v1.RequestOptions; import com.google.firestore.v1.RunQueryRequest; import com.google.firestore.v1.RunQueryResponse; import com.google.protobuf.ByteString; @@ -69,6 +70,22 @@ abstract RunQueryRequest.Builder toRunQueryRequestBuilder( @Nullable final Timestamp readTime, @Nullable ExplainOptions explainOptions); + RunQueryRequest.Builder toRunQueryRequestBuilder( + @Nullable final ByteString transactionId, + @Nullable final Timestamp readTime, + @Nullable ExplainOptions explainOptions, + @Nullable FirestoreExecutionOptions executionOptions) { + RunQueryRequest.Builder builder = + toRunQueryRequestBuilder(transactionId, readTime, explainOptions); + RequestOptions requestOptions = + RequestOptionsHelper.createRequestOptions( + rpcContext.getFirestore().getOptions(), executionOptions); + if (!requestOptions.equals(RequestOptions.getDefaultInstance())) { + builder.setRequestOptions(requestOptions); + } + return builder; + } + abstract boolean isRetryableWithCursor(); abstract StreamableQuery startAfter(@Nonnull DocumentSnapshot snapshot); @@ -105,6 +122,14 @@ MetricsContext createMetricsContext(String methodName) { */ ApiFuture get( @Nullable ByteString transactionId, @Nullable Timestamp requestReadTime) { + return get(transactionId, requestReadTime, null, null); + } + + ApiFuture get( + @Nullable ByteString transactionId, + @Nullable Timestamp requestReadTime, + @Nullable ExplainOptions explainOptions, + @Nullable FirestoreExecutionOptions executionOptions) { TraceUtil.Span span = getFirestore() .getOptions() @@ -165,7 +190,8 @@ public void onCompleted() { /* startTimeNanos= */ rpcContext.getClock().nanoTime(), transactionId, /* readTime= */ requestReadTime, - /* explainOptions= */ null, + explainOptions, + executionOptions, /* isRetryRequestWithCursor= */ false); span.endAtFuture(result); @@ -187,6 +213,19 @@ public void onCompleted() { */ @Nonnull public ApiFuture> explain(ExplainOptions options) { + return explain(options, null); + } + + @Nonnull + public ApiFuture> explain( + FirestoreExecutionOptions executionOptions) { + return explain( + executionOptions != null ? executionOptions.getExplainOptions() : null, executionOptions); + } + + @Nonnull + public ApiFuture> explain( + @Nullable ExplainOptions options, @Nullable FirestoreExecutionOptions executionOptions) { TraceUtil.Span span = getFirestore() .getOptions() @@ -254,12 +293,18 @@ public void onCompleted() { } }; + ExplainOptions effectiveExplainOptions = + options != null + ? options + : (executionOptions != null ? executionOptions.getExplainOptions() : null); + internalStream( new MonitoredStreamResponseObserver(observer, metricsContext), /* startTimeNanos= */ rpcContext.getClock().nanoTime(), /* transactionId= */ null, /* readTime= */ null, - /* explainOptions= */ options, + /* explainOptions= */ effectiveExplainOptions, + executionOptions, /* isRetryRequestWithCursor= */ false); span.endAtFuture(result); @@ -312,6 +357,24 @@ void internalStream( @Nullable final Timestamp readTime, @Nullable final ExplainOptions explainOptions, final boolean isRetryRequestWithCursor) { + internalStream( + streamResponseObserver, + startTimeNanos, + transactionId, + readTime, + explainOptions, + null, + isRetryRequestWithCursor); + } + + void internalStream( + final MonitoredStreamResponseObserver streamResponseObserver, + final long startTimeNanos, + @Nullable final ByteString transactionId, + @Nullable final Timestamp readTime, + @Nullable final ExplainOptions explainOptions, + @Nullable final FirestoreExecutionOptions executionOptions, + final boolean isRetryRequestWithCursor) { TraceUtil traceUtil = getFirestore().getOptions().getTraceUtil(); // To reduce the size of traces, we only register one event for every 100 responses @@ -388,6 +451,7 @@ public void onError(Throwable throwable) { /* transactionId= */ null, options.getRequireConsistency() ? cursor.getReadTime() : null, explainOptions, + executionOptions, /* isRetryRequestWithCursor= */ true); } else { currentSpan.addEvent( @@ -428,7 +492,7 @@ boolean shouldRetry(DocumentSnapshot lastDocument, Throwable t) { }; rpcContext.streamRequest( - toRunQueryRequestBuilder(transactionId, readTime, explainOptions).build(), + toRunQueryRequestBuilder(transactionId, readTime, explainOptions, executionOptions).build(), observer, rpcContext.getClient().runQueryCallable()); } diff --git a/java-firestore/google-cloud-firestore/src/main/java/com/google/cloud/firestore/Transaction.java b/java-firestore/google-cloud-firestore/src/main/java/com/google/cloud/firestore/Transaction.java index e11f300e069c..e369622cb193 100644 --- a/java-firestore/google-cloud-firestore/src/main/java/com/google/cloud/firestore/Transaction.java +++ b/java-firestore/google-cloud-firestore/src/main/java/com/google/cloud/firestore/Transaction.java @@ -17,6 +17,7 @@ package com.google.cloud.firestore; import com.google.api.core.ApiFuture; +import com.google.api.core.BetaApi; import com.google.api.core.InternalExtensionOnly; import com.google.cloud.firestore.pipeline.stages.PipelineExecuteOptions; import com.google.cloud.firestore.telemetry.MetricsUtil; @@ -103,6 +104,17 @@ Transaction wrapResult(int writeIndex) { @Nonnull public abstract ApiFuture get(@Nonnull DocumentReference documentRef); + /** + * Reads the document referred to by the provided DocumentReference with execution options. Holds + * a pessimistic lock on the returned document. + * + * @return The contents of the Document at this DocumentReference. + */ + @BetaApi + @Nonnull + public abstract ApiFuture get( + @Nonnull DocumentReference documentRef, @Nonnull FirestoreExecutionOptions executionOptions); + /** * Retrieves multiple documents from Firestore. Holds a pessimistic lock on all returned * documents. @@ -113,6 +125,19 @@ Transaction wrapResult(int writeIndex) { public abstract ApiFuture> getAll( @Nonnull DocumentReference... documentReferences); + /** + * Retrieves multiple documents from Firestore with execution options. Holds a pessimistic lock on + * all returned documents. + * + * @param documentReferences Array with Document References to fetch. + * @param executionOptions Options for executing the request. + */ + @BetaApi + @Nonnull + public abstract ApiFuture> getAll( + @Nonnull DocumentReference[] documentReferences, + @Nonnull FirestoreExecutionOptions executionOptions); + /** * Retrieves multiple documents from Firestore, while optionally applying a field mask to reduce * the amount of data transmitted from the backend. Holds a pessimistic lock on all returned @@ -125,6 +150,22 @@ public abstract ApiFuture> getAll( public abstract ApiFuture> getAll( @Nonnull DocumentReference[] documentReferences, @Nullable FieldMask fieldMask); + /** + * Retrieves multiple documents from Firestore with execution options, while optionally applying a + * field mask to reduce the amount of data transmitted from the backend. Holds a pessimistic lock + * on all returned documents. + * + * @param documentReferences Array with Document References to fetch. + * @param fieldMask If set, specifies the subset of fields to return. + * @param executionOptions Options for executing the request. + */ + @BetaApi + @Nonnull + public abstract ApiFuture> getAll( + @Nonnull DocumentReference[] documentReferences, + @Nullable FieldMask fieldMask, + @Nonnull FirestoreExecutionOptions executionOptions); + /** * Returns the result set from the provided query. Holds a pessimistic lock on all returned * documents. @@ -134,6 +175,17 @@ public abstract ApiFuture> getAll( @Nonnull public abstract ApiFuture get(@Nonnull Query query); + /** + * Returns the result set from the provided query with execution options. Holds a pessimistic lock + * on all returned documents. + * + * @return The contents of the Document at this DocumentReference. + */ + @BetaApi + @Nonnull + public abstract ApiFuture get( + @Nonnull Query query, @Nonnull FirestoreExecutionOptions executionOptions); + /** * Returns the result from the provided aggregate query. Holds a pessimistic lock on all accessed * documents. @@ -143,6 +195,17 @@ public abstract ApiFuture> getAll( @Nonnull public abstract ApiFuture get(@Nonnull AggregateQuery query); + /** + * Returns the result from the provided aggregate query with execution options. Holds a + * pessimistic lock on all accessed documents. + * + * @return The result of the aggregation. + */ + @BetaApi + @Nonnull + public abstract ApiFuture get( + @Nonnull AggregateQuery query, @Nonnull FirestoreExecutionOptions executionOptions); + /** * Executes the provided pipeline within the transaction context. * @@ -151,6 +214,16 @@ public abstract ApiFuture> getAll( @Nonnull public abstract ApiFuture execute(@Nonnull Pipeline pipeline); + /** + * Executes the provided pipeline with execution options within the transaction context. + * + * @return The result of the execution. + */ + @BetaApi + @Nonnull + public abstract ApiFuture execute( + @Nonnull Pipeline pipeline, @Nonnull FirestoreExecutionOptions executionOptions); + /** * Executes the provided pipeline with specified options, within the transaction context. * @@ -159,4 +232,17 @@ public abstract ApiFuture> getAll( @Nonnull public abstract ApiFuture execute( @Nonnull Pipeline pipeline, @Nonnull PipelineExecuteOptions options); + + /** + * Executes the provided pipeline with specified options and execution options, within the + * transaction context. + * + * @return The result of the execution. + */ + @BetaApi + @Nonnull + public abstract ApiFuture execute( + @Nonnull Pipeline pipeline, + @Nonnull PipelineExecuteOptions options, + @Nonnull FirestoreExecutionOptions executionOptions); } diff --git a/java-firestore/google-cloud-firestore/src/main/java/com/google/cloud/firestore/UpdateBuilder.java b/java-firestore/google-cloud-firestore/src/main/java/com/google/cloud/firestore/UpdateBuilder.java index cfa852ce4cfc..d3803fb9deb9 100644 --- a/java-firestore/google-cloud-firestore/src/main/java/com/google/cloud/firestore/UpdateBuilder.java +++ b/java-firestore/google-cloud-firestore/src/main/java/com/google/cloud/firestore/UpdateBuilder.java @@ -33,6 +33,7 @@ import com.google.common.util.concurrent.MoreExecutors; import com.google.firestore.v1.CommitRequest; import com.google.firestore.v1.CommitResponse; +import com.google.firestore.v1.RequestOptions; import com.google.firestore.v1.Write; import com.google.protobuf.ByteString; import com.google.protobuf.Timestamp; @@ -611,6 +612,12 @@ private T performDelete( /** Commit the current batch. */ ApiFuture> commit(@Nullable ByteString transactionId) { + return commit(transactionId, null); + } + + /** Commit the current batch with execution options. */ + ApiFuture> commit( + @Nullable ByteString transactionId, @Nullable FirestoreExecutionOptions executionOptions) { TraceUtil.Span span = firestore .getOptions() @@ -638,7 +645,7 @@ ApiFuture> commit(@Nullable ByteString transactionId) { // writes, we are ensured that no more writes will be appended after commit // accesses writes. committed = true; - CommitRequest request = buildCommitRequest(transactionId); + CommitRequest request = buildCommitRequest(transactionId, executionOptions); ApiFuture response = firestore.sendRequest(request, firestore.getClient().commitCallable()); @@ -661,13 +668,19 @@ ApiFuture> commit(@Nullable ByteString transactionId) { } } - private CommitRequest buildCommitRequest(ByteString transactionId) { + private CommitRequest buildCommitRequest( + ByteString transactionId, @Nullable FirestoreExecutionOptions executionOptions) { CommitRequest.Builder builder = CommitRequest.newBuilder(); builder.setDatabase(firestore.getDatabaseName()); forEachWrite(builder::addWrites); if (transactionId != null) { builder.setTransaction(transactionId); } + RequestOptions requestOptions = + RequestOptionsHelper.createRequestOptions(firestore.getOptions(), executionOptions); + if (!requestOptions.equals(RequestOptions.getDefaultInstance())) { + builder.setRequestOptions(requestOptions); + } return builder.build(); } diff --git a/java-firestore/google-cloud-firestore/src/main/java/com/google/cloud/firestore/VectorQuery.java b/java-firestore/google-cloud-firestore/src/main/java/com/google/cloud/firestore/VectorQuery.java index 205836a24388..8cae1e3df2de 100644 --- a/java-firestore/google-cloud-firestore/src/main/java/com/google/cloud/firestore/VectorQuery.java +++ b/java-firestore/google-cloud-firestore/src/main/java/com/google/cloud/firestore/VectorQuery.java @@ -17,6 +17,7 @@ package com.google.cloud.firestore; import com.google.api.core.ApiFuture; +import com.google.api.core.BetaApi; import com.google.cloud.Timestamp; import com.google.firestore.v1.RunQueryRequest; import com.google.firestore.v1.StructuredQuery; @@ -64,7 +65,19 @@ public final class VectorQuery extends StreamableQuery { @Override @Nonnull public ApiFuture get() { - return get(null, null); + return get(null, null, null, null); + } + + /** + * Executes the query with execution options and returns the results as {@link QuerySnapshot}. + * + * @param executionOptions Options for executing the request. + * @return An ApiFuture that will be resolved with the results of the VectorQuery. + */ + @BetaApi + @Nonnull + public ApiFuture get(@Nonnull FirestoreExecutionOptions executionOptions) { + return get(null, null, executionOptions.getExplainOptions(), executionOptions); } /** @@ -78,7 +91,36 @@ public ApiFuture get() { @Override @Nonnull public ApiFuture> explain(ExplainOptions options) { - return super.explain(options); + return super.explain(options, null); + } + + /** + * Plans and optionally executes this VectorQuery with execution options. + * + * @param executionOptions Options for executing the request. + * @return An ApiFuture that will be resolved with the planner information, statistics from the + * query execution (if any), and the query results (if any). + */ + @BetaApi + @Nonnull + public ApiFuture> explain( + @Nonnull FirestoreExecutionOptions executionOptions) { + return super.explain(executionOptions); + } + + /** + * Plans and optionally executes this VectorQuery with explain options and execution options. + * + * @param options The options for explain. + * @param executionOptions Options for executing the request. + * @return An ApiFuture that will be resolved with the planner information, statistics from the + * query execution (if any), and the query results (if any). + */ + @BetaApi + @Nonnull + public ApiFuture> explain( + @Nonnull ExplainOptions options, @Nonnull FirestoreExecutionOptions executionOptions) { + return super.explain(options, executionOptions); } /** diff --git a/java-firestore/google-cloud-firestore/src/main/java/com/google/cloud/firestore/WriteBatch.java b/java-firestore/google-cloud-firestore/src/main/java/com/google/cloud/firestore/WriteBatch.java index ef5435b57cac..e59149d196ce 100644 --- a/java-firestore/google-cloud-firestore/src/main/java/com/google/cloud/firestore/WriteBatch.java +++ b/java-firestore/google-cloud-firestore/src/main/java/com/google/cloud/firestore/WriteBatch.java @@ -17,6 +17,7 @@ package com.google.cloud.firestore; import com.google.api.core.ApiFuture; +import com.google.api.core.BetaApi; import java.util.List; import javax.annotation.Nonnull; @@ -41,6 +42,18 @@ public ApiFuture> commit() { return super.commit(null); } + /** + * Applies the current WriteBatch with execution options and returns an array with WriteResults. + * + * @param executionOptions Options for executing the request. + * @return ApiFuture with a List of WriteResults + */ + @BetaApi + @Nonnull + public ApiFuture> commit(@Nonnull FirestoreExecutionOptions executionOptions) { + return super.commit(null, executionOptions); + } + WriteBatch wrapResult(int writeIndex) { return this; } diff --git a/java-firestore/google-cloud-firestore/src/main/java/com/google/cloud/firestore/models/RequestOptions.java b/java-firestore/google-cloud-firestore/src/main/java/com/google/cloud/firestore/models/RequestOptions.java new file mode 100644 index 000000000000..6eec51b9c48b --- /dev/null +++ b/java-firestore/google-cloud-firestore/src/main/java/com/google/cloud/firestore/models/RequestOptions.java @@ -0,0 +1,166 @@ +/* + * Copyright 2026 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.models; + +import com.google.api.core.BetaApi; +import com.google.api.core.InternalApi; +import com.google.common.annotations.VisibleForTesting; +import com.google.common.base.Objects; +import com.google.common.base.Preconditions; +import java.util.List; +import javax.annotation.Nonnull; + +/** + * Model class for {@link com.google.firestore.v1.RequestOptions}. Contains options for a request + * such as request tags. + */ +@BetaApi +public class RequestOptions { + + private final com.google.firestore.v1.RequestOptions proto; + + private RequestOptions(com.google.firestore.v1.RequestOptions proto) { + this.proto = proto; + } + + public static Builder newBuilder() { + return new Builder(); + } + + /** Creates a {@link RequestOptions} instance from its proto representation. */ + public static RequestOptions fromProto(com.google.firestore.v1.RequestOptions proto) { + return new RequestOptions(proto); + } + + /** Returns a default {@code RequestOptions} instance. */ + public static RequestOptions getDefaultInstance() { + return newBuilder().build(); + } + + /** Returns the list of request tags. */ + @Nonnull + public List getRequestTags() { + return proto.getRequestTagsList(); + } + + /** Returns the list of request tags (alias for {@link #getRequestTags()}). */ + @Nonnull + public List getTags() { + return getRequestTags(); + } + + public Builder toBuilder() { + return new Builder(this.proto); + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (!(o instanceof RequestOptions)) { + return false; + } + RequestOptions that = (RequestOptions) o; + return Objects.equal(proto, that.proto); + } + + @Override + public int hashCode() { + return Objects.hashCode(proto); + } + + @Override + public String toString() { + return "RequestOptions{" + "requestTags=" + getRequestTags() + "}"; + } + + @InternalApi + @VisibleForTesting + public com.google.firestore.v1.RequestOptions toPb() { + return this.proto; + } + + /** Converts this instance to its protobuf representation. */ + public com.google.firestore.v1.RequestOptions toProto() { + return this.proto; + } + + public static class Builder { + + private final com.google.firestore.v1.RequestOptions.Builder protoBuilder; + + private Builder() { + this.protoBuilder = com.google.firestore.v1.RequestOptions.newBuilder(); + } + + private Builder(com.google.firestore.v1.RequestOptions proto) { + this.protoBuilder = proto.toBuilder(); + } + + /** + * Sets the request tags for the request options. + * + * @param requestTags the request tags to set + * @return this builder + */ + public Builder setRequestTags(@Nonnull List requestTags) { + Preconditions.checkNotNull(requestTags, "requestTags cannot be null"); + protoBuilder.clearRequestTags(); + protoBuilder.addAllRequestTags(requestTags); + return this; + } + + /** + * Sets the request tags for the request options (alias for {@link #setRequestTags(List)}). + * + * @param tags the request tags to set + * @return this builder + */ + public Builder setTags(@Nonnull List tags) { + return setRequestTags(tags); + } + + /** + * Adds a request tag to the request options. + * + * @param tag the request tag to add + * @return this builder + */ + public Builder addTag(@Nonnull String tag) { + Preconditions.checkNotNull(tag, "tag cannot be null"); + protoBuilder.addRequestTags(tag); + return this; + } + + /** + * Adds a request tag to the request options. + * + * @param tag the request tag to add + * @return this builder + */ + public Builder addRequestTag(@Nonnull String tag) { + return addTag(tag); + } + + /** Builds the {@link RequestOptions} instance. */ + @Nonnull + public RequestOptions build() { + return new RequestOptions(protoBuilder.build()); + } + } +} diff --git a/java-firestore/google-cloud-firestore/src/test/java/com/google/cloud/firestore/AggregateQueryTest.java b/java-firestore/google-cloud-firestore/src/test/java/com/google/cloud/firestore/AggregateQueryTest.java index 7b183a7fb57f..54a8f13f2cbf 100644 --- a/java-firestore/google-cloud-firestore/src/test/java/com/google/cloud/firestore/AggregateQueryTest.java +++ b/java-firestore/google-cloud-firestore/src/test/java/com/google/cloud/firestore/AggregateQueryTest.java @@ -27,8 +27,11 @@ import static org.mockito.Mockito.doReturn; import static org.mockito.Mockito.mock; +import com.google.cloud.firestore.models.RequestOptions; import com.google.cloud.firestore.spi.v1.FirestoreRpc; +import com.google.firestore.v1.AggregationResult; import com.google.firestore.v1.RunAggregationQueryRequest; +import com.google.firestore.v1.RunAggregationQueryResponse; import com.google.firestore.v1.StructuredQuery; import java.util.List; import java.util.Objects; @@ -194,4 +197,39 @@ public void withAlwaysUseImplicitOrderBy() throws Exception { assertThat(query.getOrderBy(0).getField().getFieldPath()).isEqualTo("a"); assertThat(query.getOrderBy(1).getField().getFieldPath()).isEqualTo("__name__"); } + + @Test + public void getWithExecutionOptions() throws Exception { + doAnswer( + (org.mockito.stubbing.Answer) + invocation -> { + com.google.api.gax.rpc.ResponseObserver observer = + (com.google.api.gax.rpc.ResponseObserver) + invocation.getArguments()[1]; + observer.onResponse( + RunAggregationQueryResponse.newBuilder() + .setResult( + AggregationResult.newBuilder() + .putAggregateFields( + "aggregate_0", + com.google.firestore.v1.Value.newBuilder() + .setIntegerValue(1) + .build())) + .build()); + observer.onComplete(); + return null; + }) + .when(firestoreMock) + .streamRequest(runQuery.capture(), any(), any()); + + FirestoreExecutionOptions executionOptions = + FirestoreExecutionOptions.options() + .withRequestOptions(RequestOptions.newBuilder().addTag("agg-tag").build()) + .build(); + + mockQuery.count().get(executionOptions).get(); + + RunAggregationQueryRequest queryRequest = runQuery.getValue(); + assertThat(queryRequest.getRequestOptions().getRequestTagsList()).containsExactly("agg-tag"); + } } diff --git a/java-firestore/google-cloud-firestore/src/test/java/com/google/cloud/firestore/DocumentReferenceTest.java b/java-firestore/google-cloud-firestore/src/test/java/com/google/cloud/firestore/DocumentReferenceTest.java index d043168b49f8..114e9f2c9ab3 100644 --- a/java-firestore/google-cloud-firestore/src/test/java/com/google/cloud/firestore/DocumentReferenceTest.java +++ b/java-firestore/google-cloud-firestore/src/test/java/com/google/cloud/firestore/DocumentReferenceTest.java @@ -71,6 +71,7 @@ import com.google.api.gax.rpc.UnaryCallable; import com.google.cloud.Timestamp; import com.google.cloud.firestore.LocalFirestoreHelper.InvalidPOJO; +import com.google.cloud.firestore.models.RequestOptions; import com.google.cloud.firestore.spi.v1.FirestoreRpc; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; @@ -1222,4 +1223,90 @@ public void deserializeCustomMap() throws ExecutionException, InterruptedExcepti assertEquals(FOO_MAP, customMap.fooMap); assertEquals(SINGLE_FIELD_OBJECT, customMap.fooMap.get("customMap")); } + + @Test + public void getWithExecutionOptions() throws Exception { + doAnswer(getAllResponse(SINGLE_FIELD_PROTO)) + .when(firestoreMock) + .streamRequest( + getAllCapture.capture(), + streamObserverCapture.capture(), + ArgumentMatchers.any()); + FirestoreExecutionOptions executionOptions = + FirestoreExecutionOptions.options() + .withRequestOptions(RequestOptions.newBuilder().addTag("test-tag").build()) + .build(); + documentReference.get(executionOptions).get(); + assertEquals( + Arrays.asList("test-tag"), + getAllCapture.getValue().getRequestOptions().getRequestTagsList()); + } + + @Test + public void setWithExecutionOptions() throws Exception { + doReturn(SINGLE_WRITE_COMMIT_RESPONSE) + .when(firestoreMock) + .sendRequest( + commitCapture.capture(), + ArgumentMatchers.>any()); + FirestoreExecutionOptions executionOptions = + FirestoreExecutionOptions.options() + .withRequestOptions(RequestOptions.newBuilder().addTag("set-tag").build()) + .build(); + documentReference.set(SINGLE_FIELD_MAP, executionOptions).get(); + assertEquals( + Arrays.asList("set-tag"), + commitCapture.getValue().getRequestOptions().getRequestTagsList()); + } + + @Test + public void createWithExecutionOptions() throws Exception { + doReturn(SINGLE_WRITE_COMMIT_RESPONSE) + .when(firestoreMock) + .sendRequest( + commitCapture.capture(), + ArgumentMatchers.>any()); + FirestoreExecutionOptions executionOptions = + FirestoreExecutionOptions.options() + .withRequestOptions(RequestOptions.newBuilder().addTag("create-tag").build()) + .build(); + documentReference.create(SINGLE_FIELD_MAP, executionOptions).get(); + assertEquals( + Arrays.asList("create-tag"), + commitCapture.getValue().getRequestOptions().getRequestTagsList()); + } + + @Test + public void updateWithExecutionOptions() throws Exception { + doReturn(SINGLE_WRITE_COMMIT_RESPONSE) + .when(firestoreMock) + .sendRequest( + commitCapture.capture(), + ArgumentMatchers.>any()); + FirestoreExecutionOptions executionOptions = + FirestoreExecutionOptions.options() + .withRequestOptions(RequestOptions.newBuilder().addTag("update-tag").build()) + .build(); + documentReference.update(SINGLE_FIELD_MAP, executionOptions).get(); + assertEquals( + Arrays.asList("update-tag"), + commitCapture.getValue().getRequestOptions().getRequestTagsList()); + } + + @Test + public void deleteWithExecutionOptions() throws Exception { + doReturn(SINGLE_WRITE_COMMIT_RESPONSE) + .when(firestoreMock) + .sendRequest( + commitCapture.capture(), + ArgumentMatchers.>any()); + FirestoreExecutionOptions executionOptions = + FirestoreExecutionOptions.options() + .withRequestOptions(RequestOptions.newBuilder().addTag("delete-tag").build()) + .build(); + documentReference.delete(executionOptions).get(); + assertEquals( + Arrays.asList("delete-tag"), + commitCapture.getValue().getRequestOptions().getRequestTagsList()); + } } diff --git a/java-firestore/google-cloud-firestore/src/test/java/com/google/cloud/firestore/FirestoreExecutionOptionsTest.java b/java-firestore/google-cloud-firestore/src/test/java/com/google/cloud/firestore/FirestoreExecutionOptionsTest.java new file mode 100644 index 000000000000..387851a45547 --- /dev/null +++ b/java-firestore/google-cloud-firestore/src/test/java/com/google/cloud/firestore/FirestoreExecutionOptionsTest.java @@ -0,0 +1,86 @@ +/* + * Copyright 2026 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.cloud.firestore; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; + +import com.google.cloud.firestore.models.RequestOptions; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + +@RunWith(JUnit4.class) +public class FirestoreExecutionOptionsTest { + + @Test + public void testDefaultInstance() { + FirestoreExecutionOptions options = FirestoreExecutionOptions.getDefaultInstance(); + assertNotNull(options); + assertNull(options.getExplainOptions()); + assertEquals(RequestOptions.getDefaultInstance(), options.getRequestOptions()); + } + + @Test + public void testWithOptions() { + ExplainOptions explainOptions = ExplainOptions.builder().setAnalyze(true).build(); + RequestOptions requestOptions = RequestOptions.newBuilder().addTag("tag1").build(); + + FirestoreExecutionOptions options = + FirestoreExecutionOptions.options() + .withExplainOptions(explainOptions) + .withRequestOptions(requestOptions) + .build(); + + assertEquals(explainOptions, options.getExplainOptions()); + assertEquals(requestOptions, options.getRequestOptions()); + } + + @Test + public void testEqualsAndHashCode() { + ExplainOptions explainOptions = ExplainOptions.builder().setAnalyze(true).build(); + RequestOptions requestOptions = RequestOptions.newBuilder().addTag("tag1").build(); + + FirestoreExecutionOptions options1 = + FirestoreExecutionOptions.options() + .withExplainOptions(explainOptions) + .withRequestOptions(requestOptions) + .build(); + FirestoreExecutionOptions options2 = + FirestoreExecutionOptions.options() + .withExplainOptions(explainOptions) + .withRequestOptions(requestOptions) + .build(); + FirestoreExecutionOptions options3 = + FirestoreExecutionOptions.options().withRequestOptions(requestOptions).build(); + + assertEquals(options1, options2); + assertEquals(options1.hashCode(), options2.hashCode()); + assertNotEquals(options1, options3); + } + + @Test + public void testToString() { + RequestOptions requestOptions = RequestOptions.newBuilder().addTag("tag1").build(); + FirestoreExecutionOptions options = + FirestoreExecutionOptions.options().withRequestOptions(requestOptions).build(); + assertTrue(options.toString().contains("tag1")); + } +} diff --git a/java-firestore/google-cloud-firestore/src/test/java/com/google/cloud/firestore/FirestoreOptionsTest.java b/java-firestore/google-cloud-firestore/src/test/java/com/google/cloud/firestore/FirestoreOptionsTest.java index d7c345f8fe29..912cf8ba12f6 100644 --- a/java-firestore/google-cloud-firestore/src/test/java/com/google/cloud/firestore/FirestoreOptionsTest.java +++ b/java-firestore/google-cloud-firestore/src/test/java/com/google/cloud/firestore/FirestoreOptionsTest.java @@ -48,4 +48,16 @@ public void instantiateWithProjectIdAndDatabaseId() { assertThat(document.getName()) .isEqualTo("projects/test-project/databases/test-database/documents/coll/doc1"); } + + @Test + public void instantiateWithRequestTags() { + java.util.List tags = java.util.Arrays.asList("tag1", "tag2"); + FirestoreOptions firestoreOptions = + FirestoreOptions.newBuilder().setProjectId("test-project").setRequestTags(tags).build(); + assertThat(firestoreOptions.getRequestTags()).containsExactly("tag1", "tag2").inOrder(); + + FirestoreOptions copiedOptions = firestoreOptions.toBuilder().build(); + assertThat(copiedOptions.getRequestTags()).containsExactly("tag1", "tag2").inOrder(); + assertThat(firestoreOptions).isEqualTo(copiedOptions); + } } diff --git a/java-firestore/google-cloud-firestore/src/test/java/com/google/cloud/firestore/FirestoreTest.java b/java-firestore/google-cloud-firestore/src/test/java/com/google/cloud/firestore/FirestoreTest.java index 638fa19ba025..ca14bf794bbe 100644 --- a/java-firestore/google-cloud-firestore/src/test/java/com/google/cloud/firestore/FirestoreTest.java +++ b/java-firestore/google-cloud-firestore/src/test/java/com/google/cloud/firestore/FirestoreTest.java @@ -35,12 +35,14 @@ import com.google.api.gax.rpc.ResponseObserver; import com.google.api.gax.rpc.ServerStreamingCallable; import com.google.api.gax.rpc.UnaryCallable; +import com.google.cloud.firestore.models.RequestOptions; import com.google.cloud.firestore.spi.v1.FirestoreRpc; import com.google.firestore.v1.BatchGetDocumentsRequest; import com.google.firestore.v1.CommitRequest; import com.google.firestore.v1.CommitResponse; import com.google.protobuf.Message; import java.util.ArrayList; +import java.util.Arrays; import java.util.Collections; import java.util.List; import java.util.concurrent.ExecutionException; @@ -222,4 +224,27 @@ public void arrayRemoveWithPojo() throws ExecutionException, InterruptedExceptio CommitRequest actualRequest = commitCapture.getValue(); assertEquals(expectedRequest, actualRequest); } + + @Test + public void getAllWithExecutionOptions() throws Exception { + doAnswer(getAllResponseWithoutOnComplete(SINGLE_FIELD_PROTO)) + .when(firestoreMock) + .streamRequest( + getAllCapture.capture(), + streamObserverCapture.capture(), + ArgumentMatchers.any()); + + FirestoreExecutionOptions executionOptions = + FirestoreExecutionOptions.options() + .withRequestOptions(RequestOptions.newBuilder().addTag("getall-tag").build()) + .build(); + + firestoreMock + .getAll(new DocumentReference[] {firestoreMock.document("coll/doc")}, executionOptions) + .get(); + + assertEquals( + Arrays.asList("getall-tag"), + getAllCapture.getValue().getRequestOptions().getRequestTagsList()); + } } diff --git a/java-firestore/google-cloud-firestore/src/test/java/com/google/cloud/firestore/QueryTest.java b/java-firestore/google-cloud-firestore/src/test/java/com/google/cloud/firestore/QueryTest.java index 51f992f399f8..76c20612b4d4 100644 --- a/java-firestore/google-cloud-firestore/src/test/java/com/google/cloud/firestore/QueryTest.java +++ b/java-firestore/google-cloud-firestore/src/test/java/com/google/cloud/firestore/QueryTest.java @@ -53,6 +53,7 @@ import com.google.cloud.firestore.Query.ComparisonFilterInternal; import com.google.cloud.firestore.Query.FieldOrder; import com.google.cloud.firestore.Query.FilterInternal; +import com.google.cloud.firestore.models.RequestOptions; import com.google.cloud.firestore.spi.v1.FirestoreRpc; import com.google.common.io.BaseEncoding; import com.google.firestore.v1.ArrayValue; @@ -1574,4 +1575,21 @@ public void inequalityFiltersImplicitlyOrderedLexicographicallyWithExplicitOrder assertEquals(orderFields, query_.createImplicitOrderBy()); } + + @Test + public void getWithExecutionOptions() throws Exception { + doAnswer(queryResponse()) + .when(firestoreMock) + .streamRequest(runQuery.capture(), streamObserverCapture.capture(), any()); + + FirestoreExecutionOptions executionOptions = + FirestoreExecutionOptions.options() + .withRequestOptions(RequestOptions.newBuilder().addTag("query-tag").build()) + .build(); + + query.get(executionOptions).get(); + + assertEquals( + Arrays.asList("query-tag"), runQuery.getValue().getRequestOptions().getRequestTagsList()); + } } diff --git a/java-firestore/google-cloud-firestore/src/test/java/com/google/cloud/firestore/RequestOptionsHelperTest.java b/java-firestore/google-cloud-firestore/src/test/java/com/google/cloud/firestore/RequestOptionsHelperTest.java new file mode 100644 index 000000000000..87413665d9d6 --- /dev/null +++ b/java-firestore/google-cloud-firestore/src/test/java/com/google/cloud/firestore/RequestOptionsHelperTest.java @@ -0,0 +1,81 @@ +/* + * Copyright 2026 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.cloud.firestore; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; + +import com.google.cloud.firestore.models.RequestOptions; +import java.util.Arrays; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + +@RunWith(JUnit4.class) +public class RequestOptionsHelperTest { + + @Test + public void testCreateRequestOptionsWithNulls() { + com.google.firestore.v1.RequestOptions proto = + RequestOptionsHelper.createRequestOptions( + (FirestoreOptions) null, (FirestoreExecutionOptions) null); + assertTrue(proto.getRequestTagsList().isEmpty()); + } + + @Test + public void testCreateRequestOptionsWithClientTagsOnly() { + FirestoreOptions options = + FirestoreOptions.newBuilder() + .setProjectId("test-project") + .setRequestTags(Arrays.asList("client-tag1", "client-tag2")) + .build(); + com.google.firestore.v1.RequestOptions proto = + RequestOptionsHelper.createRequestOptions(options, (FirestoreExecutionOptions) null); + assertEquals(Arrays.asList("client-tag1", "client-tag2"), proto.getRequestTagsList()); + } + + @Test + public void testCreateRequestOptionsWithExecutionTagsOnly() { + FirestoreExecutionOptions executionOptions = + FirestoreExecutionOptions.options() + .withRequestOptions( + RequestOptions.newBuilder().addTag("req-tag1").addTag("req-tag2").build()) + .build(); + com.google.firestore.v1.RequestOptions proto = + RequestOptionsHelper.createRequestOptions((FirestoreOptions) null, executionOptions); + assertEquals(Arrays.asList("req-tag1", "req-tag2"), proto.getRequestTagsList()); + } + + @Test + public void testCreateRequestOptionsMergedTags() { + FirestoreOptions options = + FirestoreOptions.newBuilder() + .setProjectId("test-project") + .setRequestTags(Arrays.asList("client-tag1", "client-tag2")) + .build(); + FirestoreExecutionOptions executionOptions = + FirestoreExecutionOptions.options() + .withRequestOptions( + RequestOptions.newBuilder().addTag("req-tag1").addTag("req-tag2").build()) + .build(); + com.google.firestore.v1.RequestOptions proto = + RequestOptionsHelper.createRequestOptions(options, executionOptions); + assertEquals( + Arrays.asList("req-tag1", "req-tag2", "client-tag1", "client-tag2"), + proto.getRequestTagsList()); + } +} diff --git a/java-firestore/google-cloud-firestore/src/test/java/com/google/cloud/firestore/TransactionTest.java b/java-firestore/google-cloud-firestore/src/test/java/com/google/cloud/firestore/TransactionTest.java index a21d705ba762..c401c8ae125a 100644 --- a/java-firestore/google-cloud-firestore/src/test/java/com/google/cloud/firestore/TransactionTest.java +++ b/java-firestore/google-cloud-firestore/src/test/java/com/google/cloud/firestore/TransactionTest.java @@ -60,13 +60,17 @@ import com.google.cloud.firestore.TransactionOptions.ReadOnlyOptionsBuilder; import com.google.cloud.firestore.TransactionOptions.ReadWriteOptionsBuilder; import com.google.cloud.firestore.TransactionOptions.TransactionOptionsType; +import com.google.cloud.firestore.models.RequestOptions; import com.google.cloud.firestore.spi.v1.FirestoreRpc; import com.google.firestore.v1.BatchGetDocumentsRequest; +import com.google.firestore.v1.BeginTransactionRequest; +import com.google.firestore.v1.CommitRequest; import com.google.firestore.v1.DocumentMask; import com.google.firestore.v1.Write; import com.google.protobuf.Message; import io.grpc.Status; import java.util.ArrayList; +import java.util.Arrays; import java.util.Collections; import java.util.List; import java.util.concurrent.ExecutionException; @@ -1025,4 +1029,34 @@ private ApiException exception(Status.Code code, boolean shouldRetry) { private ApiException exception(Status.Code code, String message, boolean shouldRetry) { return new ApiException(new Exception(message), GrpcStatusCode.of(code), shouldRetry); } + + @Test + public void runTransactionWithExecutionOptions() throws Exception { + doReturn(beginResponse()) + .doReturn(commitResponse(0, 0)) + .when(firestoreMock) + .sendRequest( + requestCapture.capture(), ArgumentMatchers.>any()); + + FirestoreExecutionOptions executionOptions = + FirestoreExecutionOptions.options() + .withRequestOptions(RequestOptions.newBuilder().addTag("txn-tag").build()) + .build(); + + firestoreMock + .runTransaction( + transaction -> { + return ApiFutures.immediateFuture("result"); + }, + TransactionOptions.create(), + executionOptions) + .get(); + + BeginTransactionRequest beginRequest = + (BeginTransactionRequest) requestCapture.getAllValues().get(0); + assertEquals(Arrays.asList("txn-tag"), beginRequest.getRequestOptions().getRequestTagsList()); + + CommitRequest commitRequest = (CommitRequest) requestCapture.getAllValues().get(1); + assertEquals(Arrays.asList("txn-tag"), commitRequest.getRequestOptions().getRequestTagsList()); + } } diff --git a/java-firestore/google-cloud-firestore/src/test/java/com/google/cloud/firestore/models/RequestOptionsTest.java b/java-firestore/google-cloud-firestore/src/test/java/com/google/cloud/firestore/models/RequestOptionsTest.java new file mode 100644 index 000000000000..845e441e0031 --- /dev/null +++ b/java-firestore/google-cloud-firestore/src/test/java/com/google/cloud/firestore/models/RequestOptionsTest.java @@ -0,0 +1,93 @@ +/* + * Copyright 2026 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.models; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; + +import java.util.Arrays; +import java.util.List; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + +@RunWith(JUnit4.class) +public class RequestOptionsTest { + + @Test + public void testDefaultInstance() { + RequestOptions options = RequestOptions.getDefaultInstance(); + assertNotNull(options); + assertTrue(options.getTags().isEmpty()); + assertEquals(com.google.firestore.v1.RequestOptions.getDefaultInstance(), options.toProto()); + } + + @Test + public void testBuilderWithTags() { + List tags = Arrays.asList("tag1", "tag2"); + RequestOptions options = RequestOptions.newBuilder().setTags(tags).build(); + assertEquals(tags, options.getTags()); + + com.google.firestore.v1.RequestOptions proto = options.toProto(); + assertEquals(tags, proto.getRequestTagsList()); + } + + @Test + public void testBuilderWithAddTag() { + RequestOptions options = RequestOptions.newBuilder().addTag("tag1").addTag("tag2").build(); + assertEquals(Arrays.asList("tag1", "tag2"), options.getTags()); + } + + @Test + public void testToBuilder() { + RequestOptions options = RequestOptions.newBuilder().addTag("tag1").build(); + RequestOptions updated = options.toBuilder().addTag("tag2").build(); + assertEquals(Arrays.asList("tag1", "tag2"), updated.getTags()); + } + + @Test + public void testFromProto() { + com.google.firestore.v1.RequestOptions proto = + com.google.firestore.v1.RequestOptions.newBuilder() + .addAllRequestTags(Arrays.asList("tag1", "tag2")) + .build(); + RequestOptions options = RequestOptions.fromProto(proto); + assertEquals(Arrays.asList("tag1", "tag2"), options.getTags()); + assertEquals(proto, options.toProto()); + } + + @Test + public void testEqualsAndHashCode() { + RequestOptions options1 = RequestOptions.newBuilder().addTag("tag1").addTag("tag2").build(); + RequestOptions options2 = + RequestOptions.newBuilder().setTags(Arrays.asList("tag1", "tag2")).build(); + RequestOptions options3 = RequestOptions.newBuilder().addTag("tag3").build(); + + assertEquals(options1, options2); + assertEquals(options1.hashCode(), options2.hashCode()); + assertNotEquals(options1, options3); + assertNotEquals(options1.hashCode(), options3.hashCode()); + } + + @Test + public void testToString() { + RequestOptions options = RequestOptions.newBuilder().addTag("tag1").build(); + assertTrue(options.toString().contains("tag1")); + } +}