From 2f37c4115492d7e2e4b9cd4de10d8dbb8304936f Mon Sep 17 00:00:00 2001 From: wu-hui Date: Tue, 24 Mar 2026 10:50:37 -0400 Subject: [PATCH 1/6] chore: add a simple verify skill --- .agents/skills/verify-local-changes/SKILL.md | 95 ++++++++++++++++++++ 1 file changed, 95 insertions(+) create mode 100644 .agents/skills/verify-local-changes/SKILL.md diff --git a/.agents/skills/verify-local-changes/SKILL.md b/.agents/skills/verify-local-changes/SKILL.md new file mode 100644 index 000000000..02db50857 --- /dev/null +++ b/.agents/skills/verify-local-changes/SKILL.md @@ -0,0 +1,95 @@ +--- +name: Verify Local Changes +description: Verifies local Java SDK changes. +--- + +# Verify Local Changes + +This skill documents how to verify local code changes for the Java Firestore SDK. This should be run **every time** you complete a fix or feature and are prepared to push a pull request. + +## Prerequisites + +Ensure you have Maven installed and are in the `java-firestore` directory before running commands. + +--- + +## Step 0: Format the Code + +Run the formatter to ensure formatting checks pass: + +```bash +mvn com.spotify.fmt:fmt-maven-plugin:format +``` + +--- + +## Step 1: Unit Testing (Isolated then Suite) + +1. **Identify modified unit tests** in your changes. +2. **Run specific units only** to test isolated logic regressions: + ```bash + mvn test -Dtest=MyUnitTest#testMethod + ``` +3. **Run the entire unit test suite** that contains those modified tests if the isolated unit tests pass: + ```bash + mvn test -Dtest=MyUnitTest + ``` + +--- + +## Step 2: Integration Testing (Isolated then Suite) + +### 💡 Integration Test Nuances (from `ITBaseTest.java`) + +When running integration tests, configure your execution using properties or environment variables: + +- **`FIRESTORE_EDITION`**: + - `standard` (Default) + - `enterprise` + - *Note*: **Pipelines can only be run against `enterprise` editions**, while standard Queries run on both. +- **`FIRESTORE_NAMED_DATABASE`**: + - Enterprise editions usually require a named database (often `enterprise`). Adjust this flag if pointing to specific instances. +- **`FIRESTORE_TARGET_BACKEND`**: + - `PROD` (Default) + - `QA` (points to standard sandboxes) + - `NIGHTLY` (points to `test-firestore.sandbox.googleapis.com:443`) + - `EMULATOR` (points to `localhost:8080`) + +1. **Identify modified integration tests** (usually Starting in `IT`). +2. **Run specific integration tests only** (isolated checks run quicker): + ```bash + mvn verify -Penable-integration-tests -DFIRESTORE_EDITION=enterprise -DFIRESTORE_NAMED_DATABASE=enterprise -Dtest=ITTest#testMethod -Dclirr.skip=true -Denforcer.skip=true -fae + ``` +3. **Run the entire integration test suite** for the modified class if isolation tests pass: + ```bash + mvn verify -Penable-integration-tests -DFIRESTORE_EDITION=enterprise -DFIRESTORE_NAMED_DATABASE=enterprise -Dtest=ITTest -Dclirr.skip=true -Denforcer.skip=true -fae + ``` + + + +--- + +## Step 3: Full Suite Regressions + +Run the full integration regression suite once you are confident subsets pass: + +```bash +mvn verify -Penable-integration-tests -DFIRESTORE_EDITION=enterprise -DFIRESTORE_NAMED_DATABASE=enterprise -Dclirr.skip=true -Denforcer.skip=true -fae +``` + +--- + +> [!TIP] +> Use `-Dclirr.skip=true -Denforcer.skip=true` to speed up iterations where appropriate without leaking compliance checks. + +--- + +## 🛠️ Troubleshooting & Source of Truth + +If you run into issues executing tests with the commands above, **consult the Kokoro configuration files** as the ultimate source of truth: + +- **Presubmit configurations**: See `.kokoro/presubmit/integration.cfg` (or `integration-named-db.cfg`) +- **Nightly configurations**: See `.kokoro/nightly/integration.cfg` +- **Build shell scripts**: See `.kokoro/build.sh` + +These files define the exact environment variables (e.g., specific endpoints or endpoints overrides) the CI server uses! From 81e6857bddf6e2de789bdfbb2275f7c47a590488 Mon Sep 17 00:00:00 2001 From: wu-hui Date: Wed, 11 Feb 2026 13:45:23 -0500 Subject: [PATCH 2/6] feat: DML prototype --- .../com/google/cloud/firestore/Pipeline.java | 105 ++++++++++++++++++ .../pipeline/stages/ConflictResolution.java | 35 ++++++ .../firestore/pipeline/stages/Delete.java | 68 ++++++++++++ .../pipeline/stages/DeleteOptions.java | 38 +++++++ .../pipeline/stages/DeleteReturn.java | 33 ++++++ .../firestore/pipeline/stages/Insert.java | 78 +++++++++++++ .../pipeline/stages/InsertOptions.java | 38 +++++++ .../pipeline/stages/InsertReturn.java | 33 ++++++ .../firestore/pipeline/stages/Upsert.java | 78 +++++++++++++ .../pipeline/stages/UpsertOptions.java | 49 ++++++++ .../pipeline/stages/UpsertReturn.java | 33 ++++++ .../pipeline/stages/WriteOptions.java | 39 +++++++ .../cloud/firestore/it/ITPipelineTest.java | 83 ++++++++++++++ 13 files changed, 710 insertions(+) create mode 100644 google-cloud-firestore/src/main/java/com/google/cloud/firestore/pipeline/stages/ConflictResolution.java create mode 100644 google-cloud-firestore/src/main/java/com/google/cloud/firestore/pipeline/stages/Delete.java create mode 100644 google-cloud-firestore/src/main/java/com/google/cloud/firestore/pipeline/stages/DeleteOptions.java create mode 100644 google-cloud-firestore/src/main/java/com/google/cloud/firestore/pipeline/stages/DeleteReturn.java create mode 100644 google-cloud-firestore/src/main/java/com/google/cloud/firestore/pipeline/stages/Insert.java create mode 100644 google-cloud-firestore/src/main/java/com/google/cloud/firestore/pipeline/stages/InsertOptions.java create mode 100644 google-cloud-firestore/src/main/java/com/google/cloud/firestore/pipeline/stages/InsertReturn.java create mode 100644 google-cloud-firestore/src/main/java/com/google/cloud/firestore/pipeline/stages/Upsert.java create mode 100644 google-cloud-firestore/src/main/java/com/google/cloud/firestore/pipeline/stages/UpsertOptions.java create mode 100644 google-cloud-firestore/src/main/java/com/google/cloud/firestore/pipeline/stages/UpsertReturn.java create mode 100644 google-cloud-firestore/src/main/java/com/google/cloud/firestore/pipeline/stages/WriteOptions.java diff --git a/google-cloud-firestore/src/main/java/com/google/cloud/firestore/Pipeline.java b/google-cloud-firestore/src/main/java/com/google/cloud/firestore/Pipeline.java index c0f4c1c46..16c0e2215 100644 --- a/google-cloud-firestore/src/main/java/com/google/cloud/firestore/Pipeline.java +++ b/google-cloud-firestore/src/main/java/com/google/cloud/firestore/Pipeline.java @@ -41,9 +41,13 @@ import com.google.cloud.firestore.pipeline.stages.AddFields; import com.google.cloud.firestore.pipeline.stages.Aggregate; import com.google.cloud.firestore.pipeline.stages.AggregateOptions; +import com.google.cloud.firestore.pipeline.stages.Delete; +import com.google.cloud.firestore.pipeline.stages.DeleteOptions; import com.google.cloud.firestore.pipeline.stages.Distinct; import com.google.cloud.firestore.pipeline.stages.FindNearest; import com.google.cloud.firestore.pipeline.stages.FindNearestOptions; +import com.google.cloud.firestore.pipeline.stages.Insert; +import com.google.cloud.firestore.pipeline.stages.InsertOptions; import com.google.cloud.firestore.pipeline.stages.Limit; import com.google.cloud.firestore.pipeline.stages.Offset; import com.google.cloud.firestore.pipeline.stages.PipelineExecuteOptions; @@ -58,6 +62,8 @@ import com.google.cloud.firestore.pipeline.stages.Union; import com.google.cloud.firestore.pipeline.stages.Unnest; import com.google.cloud.firestore.pipeline.stages.UnnestOptions; +import com.google.cloud.firestore.pipeline.stages.Upsert; +import com.google.cloud.firestore.pipeline.stages.UpsertOptions; import com.google.cloud.firestore.pipeline.stages.Where; import com.google.cloud.firestore.telemetry.MetricsUtil.MetricsContext; import com.google.cloud.firestore.telemetry.TelemetryConstants; @@ -995,6 +1001,105 @@ public Pipeline unnest(Selectable field, UnnestOptions options) { return append(new Unnest(field, options)); } + /** + * Performs a delete operation on documents from previous stages. + * + * @return A new {@code Pipeline} object with this stage appended to the stage list. + */ + @BetaApi + public Pipeline delete() { + return append(new Delete()); + } + + /** + * Performs a delete operation on documents from previous stages. + * + * @param target The collection to delete from. + * @return A new {@code Pipeline} object with this stage appended to the stage list. + */ + @BetaApi + public Pipeline delete(CollectionReference target) { + return append(Delete.withCollection(target)); + } + + /** + * Performs a delete operation on documents from previous stages. + * + * @param deleteStage The {@code Delete} stage to append. + * @param options The {@code DeleteOptions} to apply to the stage. + * @return A new {@code Pipeline} object with this stage appended to the stage list. + */ + @BetaApi + public Pipeline delete(Delete deleteStage, DeleteOptions options) { + return append(deleteStage.withOptions(options)); + } + + /** + * Performs an upsert operation using documents from previous stages. + * + * @return A new {@code Pipeline} object with this stage appended to the stage list. + */ + @BetaApi + public Pipeline upsert() { + return append(new Upsert()); + } + + /** + * Performs an upsert operation using documents from previous stages. + * + * @param target The collection to upsert to. + * @return A new {@code Pipeline} object with this stage appended to the stage list. + */ + @BetaApi + public Pipeline upsert(CollectionReference target) { + return append(Upsert.withCollection(target)); + } + + /** + * Performs an upsert operation using documents from previous stages. + * + * @param upsertStage The {@code Upsert} stage to append. + * @param options The {@code UpsertOptions} to apply to the stage. + * @return A new {@code Pipeline} object with this stage appended to the stage list. + */ + @BetaApi + public Pipeline upsert(Upsert upsertStage, UpsertOptions options) { + return append(upsertStage.withOptions(options)); + } + + /** + * Performs an insert operation using documents from previous stages. + * + * @return A new {@code Pipeline} object with this stage appended to the stage list. + */ + @BetaApi + public Pipeline insert() { + return append(new Insert()); + } + + /** + * Performs an insert operation using documents from previous stages. + * + * @param target The collection to insert to. + * @return A new {@code Pipeline} object with this stage appended to the stage list. + */ + @BetaApi + public Pipeline insert(CollectionReference target) { + return append(Insert.withCollection(target)); + } + + /** + * Performs an insert operation using documents from previous stages. + * + * @param insertStage The {@code Insert} stage to append. + * @param options The {@code InsertOptions} to apply to the stage. + * @return A new {@code Pipeline} object with this stage appended to the stage list. + */ + @BetaApi + public Pipeline insert(Insert insertStage, InsertOptions options) { + return append(insertStage.withOptions(options)); + } + /** * Adds a generic stage to the pipeline. * diff --git a/google-cloud-firestore/src/main/java/com/google/cloud/firestore/pipeline/stages/ConflictResolution.java b/google-cloud-firestore/src/main/java/com/google/cloud/firestore/pipeline/stages/ConflictResolution.java new file mode 100644 index 000000000..8766b7c05 --- /dev/null +++ b/google-cloud-firestore/src/main/java/com/google/cloud/firestore/pipeline/stages/ConflictResolution.java @@ -0,0 +1,35 @@ +/* + * 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.pipeline.stages; + +/** Defines the conflict resolution options for an Upsert pipeline stage. */ +public enum ConflictResolution { + OVERWRITE("OVERWRITE"), + MERGE("MERGE"), + FAIL("FAIL"), + KEEP("KEEP"); + + private final String value; + + ConflictResolution(String value) { + this.value = value; + } + + public String getValue() { + return value; + } +} diff --git a/google-cloud-firestore/src/main/java/com/google/cloud/firestore/pipeline/stages/Delete.java b/google-cloud-firestore/src/main/java/com/google/cloud/firestore/pipeline/stages/Delete.java new file mode 100644 index 000000000..60dbeabac --- /dev/null +++ b/google-cloud-firestore/src/main/java/com/google/cloud/firestore/pipeline/stages/Delete.java @@ -0,0 +1,68 @@ +/* + * Copyright 2024 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.cloud.firestore.pipeline.stages; + +import com.google.api.core.BetaApi; +import com.google.api.core.InternalApi; +import com.google.cloud.firestore.CollectionReference; +import com.google.cloud.firestore.PipelineUtils; +import com.google.firestore.v1.Value; +import java.util.ArrayList; +import java.util.List; +import javax.annotation.Nullable; + +@InternalApi +public final class Delete extends Stage { + + @Nullable private final String path; + + private Delete(@Nullable String path, InternalOptions options) { + super("delete", options); + this.path = path; + } + + @BetaApi + public Delete() { + this(null, InternalOptions.EMPTY); + } + + @BetaApi + public static Delete withCollection(CollectionReference target) { + String path = target.getPath(); + return new Delete(path.startsWith("/") ? path : "/" + path, InternalOptions.EMPTY); + } + + @BetaApi + public Delete withOptions(DeleteOptions options) { + return new Delete(path, this.options.adding(options)); + } + + @BetaApi + public Delete withReturns(DeleteReturn returns) { + return new Delete( + path, this.options.with("returns", PipelineUtils.encodeValue(returns.getValue()))); + } + + @Override + Iterable toStageArgs() { + List args = new ArrayList<>(); + if (path != null) { + args.add(Value.newBuilder().setReferenceValue(path).build()); + } + return args; + } +} diff --git a/google-cloud-firestore/src/main/java/com/google/cloud/firestore/pipeline/stages/DeleteOptions.java b/google-cloud-firestore/src/main/java/com/google/cloud/firestore/pipeline/stages/DeleteOptions.java new file mode 100644 index 000000000..649e575b9 --- /dev/null +++ b/google-cloud-firestore/src/main/java/com/google/cloud/firestore/pipeline/stages/DeleteOptions.java @@ -0,0 +1,38 @@ +/* + * Copyright 2024 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.cloud.firestore.pipeline.stages; + +import com.google.api.core.BetaApi; + +/** Options for a Delete pipeline stage. */ +@BetaApi +public class DeleteOptions extends WriteOptions { + + /** Creates a new, empty `DeleteOptions` object. */ + public DeleteOptions() { + super(InternalOptions.EMPTY); + } + + DeleteOptions(InternalOptions options) { + super(options); + } + + @Override + DeleteOptions self(InternalOptions options) { + return new DeleteOptions(options); + } +} diff --git a/google-cloud-firestore/src/main/java/com/google/cloud/firestore/pipeline/stages/DeleteReturn.java b/google-cloud-firestore/src/main/java/com/google/cloud/firestore/pipeline/stages/DeleteReturn.java new file mode 100644 index 000000000..d0fefbbd6 --- /dev/null +++ b/google-cloud-firestore/src/main/java/com/google/cloud/firestore/pipeline/stages/DeleteReturn.java @@ -0,0 +1,33 @@ +/* + * 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.pipeline.stages; + +/** Defines the return value options for a Delete pipeline stage. */ +public enum DeleteReturn { + EMPTY("EMPTY"), + DOCUMENT_ID("DOCUMENT_ID"); + + private final String value; + + DeleteReturn(String value) { + this.value = value; + } + + public String getValue() { + return value; + } +} diff --git a/google-cloud-firestore/src/main/java/com/google/cloud/firestore/pipeline/stages/Insert.java b/google-cloud-firestore/src/main/java/com/google/cloud/firestore/pipeline/stages/Insert.java new file mode 100644 index 000000000..04555740a --- /dev/null +++ b/google-cloud-firestore/src/main/java/com/google/cloud/firestore/pipeline/stages/Insert.java @@ -0,0 +1,78 @@ +/* + * Copyright 2024 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.cloud.firestore.pipeline.stages; + +import com.google.api.core.BetaApi; +import com.google.api.core.InternalApi; +import com.google.cloud.firestore.CollectionReference; +import com.google.cloud.firestore.PipelineUtils; +import com.google.cloud.firestore.pipeline.expressions.Selectable; +import com.google.firestore.v1.Value; +import java.util.ArrayList; +import java.util.List; +import javax.annotation.Nullable; + +@InternalApi +public final class Insert extends Stage { + + @Nullable private final String path; + + private Insert(@Nullable String path, InternalOptions options) { + super("insert", options); + this.path = path; + } + + @BetaApi + public Insert() { + this(null, InternalOptions.EMPTY); + } + + @BetaApi + public static Insert withCollection(CollectionReference target) { + String path = target.getPath(); + return new Insert(path.startsWith("/") ? path : "/" + path, InternalOptions.EMPTY); + } + + @BetaApi + public Insert withOptions(InsertOptions options) { + return new Insert(path, this.options.adding(options)); + } + + @BetaApi + public Insert withReturns(InsertReturn returns) { + return new Insert( + path, this.options.with("returns", PipelineUtils.encodeValue(returns.getValue()))); + } + + @BetaApi + public Insert withTransformations(Selectable... transformations) { + return new Insert( + path, + this.options.with( + "transformations", + PipelineUtils.encodeValue(PipelineUtils.selectablesToMap(transformations)))); + } + + @Override + Iterable toStageArgs() { + List args = new ArrayList<>(); + if (path != null) { + args.add(Value.newBuilder().setReferenceValue(path).build()); + } + return args; + } +} diff --git a/google-cloud-firestore/src/main/java/com/google/cloud/firestore/pipeline/stages/InsertOptions.java b/google-cloud-firestore/src/main/java/com/google/cloud/firestore/pipeline/stages/InsertOptions.java new file mode 100644 index 000000000..ee5c3f65e --- /dev/null +++ b/google-cloud-firestore/src/main/java/com/google/cloud/firestore/pipeline/stages/InsertOptions.java @@ -0,0 +1,38 @@ +/* + * Copyright 2024 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.cloud.firestore.pipeline.stages; + +import com.google.api.core.BetaApi; + +/** Options for an Insert pipeline stage. */ +@BetaApi +public class InsertOptions extends WriteOptions { + + /** Creates a new, empty `InsertOptions` object. */ + public InsertOptions() { + super(InternalOptions.EMPTY); + } + + InsertOptions(InternalOptions options) { + super(options); + } + + @Override + InsertOptions self(InternalOptions options) { + return new InsertOptions(options); + } +} diff --git a/google-cloud-firestore/src/main/java/com/google/cloud/firestore/pipeline/stages/InsertReturn.java b/google-cloud-firestore/src/main/java/com/google/cloud/firestore/pipeline/stages/InsertReturn.java new file mode 100644 index 000000000..bd44ec2f7 --- /dev/null +++ b/google-cloud-firestore/src/main/java/com/google/cloud/firestore/pipeline/stages/InsertReturn.java @@ -0,0 +1,33 @@ +/* + * 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.pipeline.stages; + +/** Defines the return value options for an Insert pipeline stage. */ +public enum InsertReturn { + EMPTY("EMPTY"), + DOCUMENT_ID("DOCUMENT_ID"); + + private final String value; + + InsertReturn(String value) { + this.value = value; + } + + public String getValue() { + return value; + } +} diff --git a/google-cloud-firestore/src/main/java/com/google/cloud/firestore/pipeline/stages/Upsert.java b/google-cloud-firestore/src/main/java/com/google/cloud/firestore/pipeline/stages/Upsert.java new file mode 100644 index 000000000..8dd9c1763 --- /dev/null +++ b/google-cloud-firestore/src/main/java/com/google/cloud/firestore/pipeline/stages/Upsert.java @@ -0,0 +1,78 @@ +/* + * Copyright 2024 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.cloud.firestore.pipeline.stages; + +import com.google.api.core.BetaApi; +import com.google.api.core.InternalApi; +import com.google.cloud.firestore.CollectionReference; +import com.google.cloud.firestore.PipelineUtils; +import com.google.cloud.firestore.pipeline.expressions.Selectable; +import com.google.firestore.v1.Value; +import java.util.ArrayList; +import java.util.List; +import javax.annotation.Nullable; + +@InternalApi +public final class Upsert extends Stage { + + @Nullable private final String path; + + private Upsert(@Nullable String path, InternalOptions options) { + super("upsert", options); + this.path = path; + } + + @BetaApi + public Upsert() { + this(null, InternalOptions.EMPTY); + } + + @BetaApi + public static Upsert withCollection(CollectionReference target) { + String path = target.getPath(); + return new Upsert(path.startsWith("/") ? path : "/" + path, InternalOptions.EMPTY); + } + + @BetaApi + public Upsert withOptions(UpsertOptions options) { + return new Upsert(path, this.options.adding(options)); + } + + @BetaApi + public Upsert withReturns(UpsertReturn returns) { + return new Upsert( + path, this.options.with("returns", PipelineUtils.encodeValue(returns.getValue()))); + } + + @BetaApi + public Upsert withTransformations(Selectable... transformations) { + return new Upsert( + path, + this.options.with( + "transformations", + PipelineUtils.encodeValue(PipelineUtils.selectablesToMap(transformations)))); + } + + @Override + Iterable toStageArgs() { + List args = new ArrayList<>(); + if (path != null) { + args.add(Value.newBuilder().setReferenceValue(path).build()); + } + return args; + } +} diff --git a/google-cloud-firestore/src/main/java/com/google/cloud/firestore/pipeline/stages/UpsertOptions.java b/google-cloud-firestore/src/main/java/com/google/cloud/firestore/pipeline/stages/UpsertOptions.java new file mode 100644 index 000000000..98844e23e --- /dev/null +++ b/google-cloud-firestore/src/main/java/com/google/cloud/firestore/pipeline/stages/UpsertOptions.java @@ -0,0 +1,49 @@ +/* + * Copyright 2024 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.cloud.firestore.pipeline.stages; + +import com.google.api.core.BetaApi; + +/** Options for an Upsert pipeline stage. */ +@BetaApi +public class UpsertOptions extends WriteOptions { + + /** Creates a new, empty `UpsertOptions` object. */ + public UpsertOptions() { + super(InternalOptions.EMPTY); + } + + UpsertOptions(InternalOptions options) { + super(options); + } + + @Override + UpsertOptions self(InternalOptions options) { + return new UpsertOptions(options); + } + + /** + * Sets the conflict resolution strategy. + * + * @param conflictResolution The conflict resolution strategy. + * @return A new options object with the conflict resolution set. + */ + @BetaApi + public UpsertOptions withConflictResolution(ConflictResolution conflictResolution) { + return with("conflict_resolution", conflictResolution.getValue()); + } +} diff --git a/google-cloud-firestore/src/main/java/com/google/cloud/firestore/pipeline/stages/UpsertReturn.java b/google-cloud-firestore/src/main/java/com/google/cloud/firestore/pipeline/stages/UpsertReturn.java new file mode 100644 index 000000000..4f7ee7089 --- /dev/null +++ b/google-cloud-firestore/src/main/java/com/google/cloud/firestore/pipeline/stages/UpsertReturn.java @@ -0,0 +1,33 @@ +/* + * 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.pipeline.stages; + +/** Defines the return value options for an Upsert pipeline stage. */ +public enum UpsertReturn { + EMPTY("EMPTY"), + DOCUMENT_ID("DOCUMENT_ID"); + + private final String value; + + UpsertReturn(String value) { + this.value = value; + } + + public String getValue() { + return value; + } +} diff --git a/google-cloud-firestore/src/main/java/com/google/cloud/firestore/pipeline/stages/WriteOptions.java b/google-cloud-firestore/src/main/java/com/google/cloud/firestore/pipeline/stages/WriteOptions.java new file mode 100644 index 000000000..ac4dd92c1 --- /dev/null +++ b/google-cloud-firestore/src/main/java/com/google/cloud/firestore/pipeline/stages/WriteOptions.java @@ -0,0 +1,39 @@ +/* + * Copyright 2024 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.cloud.firestore.pipeline.stages; + +import com.google.api.core.BetaApi; + +/** Options for write stages in a pipeline. */ +@BetaApi +public abstract class WriteOptions> extends AbstractOptions { + + WriteOptions(InternalOptions options) { + super(options); + } + + /** + * Sets the transactional option. + * + * @param transactional Whether the operation should be transactional. + * @return A new options object with the transactional option set. + */ + @BetaApi + public T withTransactional(boolean transactional) { + return with("transactional", transactional); + } +} diff --git a/google-cloud-firestore/src/test/java/com/google/cloud/firestore/it/ITPipelineTest.java b/google-cloud-firestore/src/test/java/com/google/cloud/firestore/it/ITPipelineTest.java index c9b78f11a..39fc33e6c 100644 --- a/google-cloud-firestore/src/test/java/com/google/cloud/firestore/it/ITPipelineTest.java +++ b/google-cloud-firestore/src/test/java/com/google/cloud/firestore/it/ITPipelineTest.java @@ -116,14 +116,24 @@ import com.google.cloud.firestore.pipeline.stages.AggregateOptions; import com.google.cloud.firestore.pipeline.stages.CollectionHints; import com.google.cloud.firestore.pipeline.stages.CollectionOptions; +import com.google.cloud.firestore.pipeline.stages.ConflictResolution; +import com.google.cloud.firestore.pipeline.stages.Delete; +import com.google.cloud.firestore.pipeline.stages.DeleteOptions; +import com.google.cloud.firestore.pipeline.stages.DeleteReturn; import com.google.cloud.firestore.pipeline.stages.ExplainOptions; import com.google.cloud.firestore.pipeline.stages.FindNearest; import com.google.cloud.firestore.pipeline.stages.FindNearestOptions; +import com.google.cloud.firestore.pipeline.stages.Insert; +import com.google.cloud.firestore.pipeline.stages.InsertOptions; +import com.google.cloud.firestore.pipeline.stages.InsertReturn; import com.google.cloud.firestore.pipeline.stages.PipelineExecuteOptions; import com.google.cloud.firestore.pipeline.stages.RawOptions; import com.google.cloud.firestore.pipeline.stages.RawStage; import com.google.cloud.firestore.pipeline.stages.Sample; import com.google.cloud.firestore.pipeline.stages.UnnestOptions; +import com.google.cloud.firestore.pipeline.stages.Upsert; +import com.google.cloud.firestore.pipeline.stages.UpsertOptions; +import com.google.cloud.firestore.pipeline.stages.UpsertReturn; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; import com.google.common.collect.Lists; @@ -3310,6 +3320,79 @@ public void testUnion() throws Exception { assertThat(results).hasSize(22); } + @Test + public void testDelete() throws Exception { + firestore + .pipeline() + .collection(collection) + .where(equal("title", "The Hitchhiker's Guide to the Galaxy")) + .delete() + .execute() + .get(); + + firestore + .pipeline() + .collection(collection) + .where(equal("title", "The Hitchhiker's Guide to the Galaxy")) + .delete( + new Delete().withReturns(DeleteReturn.DOCUMENT_ID), + new DeleteOptions().withTransactional(true)) + .execute() + .get(); + } + + @Test + public void testUpsert() throws Exception { + firestore + .pipeline() + .collection(collection) + .where(equal("title", "The Hitchhiker's Guide to the Galaxy")) + .upsert() + .execute() + .get(); + + firestore + .pipeline() + .collection(collection) + .where(equal("title", "The Hitchhiker's Guide to the Galaxy")) + .upsert(collection) + .execute() + .get(); + + firestore + .pipeline() + .collection(collection) + .where(equal("title", "The Hitchhiker's Guide to the Galaxy")) + .upsert( + new Upsert().withReturns(UpsertReturn.DOCUMENT_ID), + new UpsertOptions() + .withConflictResolution(ConflictResolution.MERGE) + .withTransactional(true)) + .execute() + .get(); + } + + @Test + public void testInsert() throws Exception { + firestore + .pipeline() + .collection(collection) + .where(equal("title", "The Hitchhiker's Guide to the Galaxy")) + .insert(collection) + .execute() + .get(); + + firestore + .pipeline() + .collection(collection) + .where(equal("title", "The Hitchhiker's Guide to the Galaxy")) + .insert( + new Insert().withReturns(InsertReturn.DOCUMENT_ID), + new InsertOptions().withTransactional(true)) + .execute() + .get(); + } + @Test public void testUnnest() throws Exception { List results = From 5f25f122579742d881c49a6537645baf241d5f27 Mon Sep 17 00:00:00 2001 From: Daniel La Rocque Date: Mon, 9 Mar 2026 15:45:01 -0400 Subject: [PATCH 3/6] make options and returns internal, add update stage (#2335) * make options and returns internal, add update stage * chore: generate libraries at Wed Mar 4 16:26:17 UTC 2026 --------- Co-authored-by: cloud-java-bot --- .../com/google/cloud/firestore/Pipeline.java | 41 ++++- .../firestore/pipeline/stages/Delete.java | 4 +- .../pipeline/stages/DeleteOptions.java | 4 +- .../pipeline/stages/DeleteReturn.java | 3 + .../firestore/pipeline/stages/Insert.java | 4 +- .../pipeline/stages/InsertOptions.java | 4 +- .../pipeline/stages/InsertReturn.java | 3 + .../firestore/pipeline/stages/Update.java | 78 ++++++++++ .../pipeline/stages/UpdateOptions.java | 49 ++++++ .../pipeline/stages/UpdateReturn.java | 36 +++++ .../firestore/pipeline/stages/Upsert.java | 4 +- .../pipeline/stages/UpsertOptions.java | 6 +- .../pipeline/stages/UpsertReturn.java | 3 + .../cloud/firestore/it/ITPipelineTest.java | 142 ++++++++---------- 14 files changed, 282 insertions(+), 99 deletions(-) create mode 100644 google-cloud-firestore/src/main/java/com/google/cloud/firestore/pipeline/stages/Update.java create mode 100644 google-cloud-firestore/src/main/java/com/google/cloud/firestore/pipeline/stages/UpdateOptions.java create mode 100644 google-cloud-firestore/src/main/java/com/google/cloud/firestore/pipeline/stages/UpdateReturn.java diff --git a/google-cloud-firestore/src/main/java/com/google/cloud/firestore/Pipeline.java b/google-cloud-firestore/src/main/java/com/google/cloud/firestore/Pipeline.java index 16c0e2215..72f416a3e 100644 --- a/google-cloud-firestore/src/main/java/com/google/cloud/firestore/Pipeline.java +++ b/google-cloud-firestore/src/main/java/com/google/cloud/firestore/Pipeline.java @@ -62,6 +62,8 @@ import com.google.cloud.firestore.pipeline.stages.Union; import com.google.cloud.firestore.pipeline.stages.Unnest; import com.google.cloud.firestore.pipeline.stages.UnnestOptions; +import com.google.cloud.firestore.pipeline.stages.Update; +import com.google.cloud.firestore.pipeline.stages.UpdateOptions; import com.google.cloud.firestore.pipeline.stages.Upsert; import com.google.cloud.firestore.pipeline.stages.UpsertOptions; import com.google.cloud.firestore.pipeline.stages.Where; @@ -1029,7 +1031,7 @@ public Pipeline delete(CollectionReference target) { * @param options The {@code DeleteOptions} to apply to the stage. * @return A new {@code Pipeline} object with this stage appended to the stage list. */ - @BetaApi + @InternalApi public Pipeline delete(Delete deleteStage, DeleteOptions options) { return append(deleteStage.withOptions(options)); } @@ -1062,11 +1064,44 @@ public Pipeline upsert(CollectionReference target) { * @param options The {@code UpsertOptions} to apply to the stage. * @return A new {@code Pipeline} object with this stage appended to the stage list. */ - @BetaApi + @InternalApi public Pipeline upsert(Upsert upsertStage, UpsertOptions options) { return append(upsertStage.withOptions(options)); } + /** + * Performs an update operation using documents from previous stages. + * + * @return A new {@code Pipeline} object with this stage appended to the stage list. + */ + @BetaApi + public Pipeline update() { + return append(new Update()); + } + + /** + * Performs an update operation using documents from previous stages. + * + * @param transformations The transformations to apply. + * @return A new {@code Pipeline} object with this stage appended to the stage list. + */ + @BetaApi + public Pipeline update(Selectable... transformations) { + return append(new Update().withTransformations(transformations)); + } + + /** + * Performs an update operation using documents from previous stages. + * + * @param update The {@code Update} stage to append. + * @param options The {@code UpdateOptions} to apply to the stage. + * @return A new {@code Pipeline} object with this stage appended to the stage list. + */ + @InternalApi + public Pipeline update(Update updateStage, UpdateOptions options) { + return append(updateStage.withOptions(options)); + } + /** * Performs an insert operation using documents from previous stages. * @@ -1095,7 +1130,7 @@ public Pipeline insert(CollectionReference target) { * @param options The {@code InsertOptions} to apply to the stage. * @return A new {@code Pipeline} object with this stage appended to the stage list. */ - @BetaApi + @InternalApi public Pipeline insert(Insert insertStage, InsertOptions options) { return append(insertStage.withOptions(options)); } diff --git a/google-cloud-firestore/src/main/java/com/google/cloud/firestore/pipeline/stages/Delete.java b/google-cloud-firestore/src/main/java/com/google/cloud/firestore/pipeline/stages/Delete.java index 60dbeabac..b72e89e86 100644 --- a/google-cloud-firestore/src/main/java/com/google/cloud/firestore/pipeline/stages/Delete.java +++ b/google-cloud-firestore/src/main/java/com/google/cloud/firestore/pipeline/stages/Delete.java @@ -46,12 +46,12 @@ public static Delete withCollection(CollectionReference target) { return new Delete(path.startsWith("/") ? path : "/" + path, InternalOptions.EMPTY); } - @BetaApi + @InternalApi public Delete withOptions(DeleteOptions options) { return new Delete(path, this.options.adding(options)); } - @BetaApi + @InternalApi public Delete withReturns(DeleteReturn returns) { return new Delete( path, this.options.with("returns", PipelineUtils.encodeValue(returns.getValue()))); diff --git a/google-cloud-firestore/src/main/java/com/google/cloud/firestore/pipeline/stages/DeleteOptions.java b/google-cloud-firestore/src/main/java/com/google/cloud/firestore/pipeline/stages/DeleteOptions.java index 649e575b9..356212920 100644 --- a/google-cloud-firestore/src/main/java/com/google/cloud/firestore/pipeline/stages/DeleteOptions.java +++ b/google-cloud-firestore/src/main/java/com/google/cloud/firestore/pipeline/stages/DeleteOptions.java @@ -16,10 +16,10 @@ package com.google.cloud.firestore.pipeline.stages; -import com.google.api.core.BetaApi; +import com.google.api.core.InternalApi; /** Options for a Delete pipeline stage. */ -@BetaApi +@InternalApi public class DeleteOptions extends WriteOptions { /** Creates a new, empty `DeleteOptions` object. */ diff --git a/google-cloud-firestore/src/main/java/com/google/cloud/firestore/pipeline/stages/DeleteReturn.java b/google-cloud-firestore/src/main/java/com/google/cloud/firestore/pipeline/stages/DeleteReturn.java index d0fefbbd6..ff81f44e8 100644 --- a/google-cloud-firestore/src/main/java/com/google/cloud/firestore/pipeline/stages/DeleteReturn.java +++ b/google-cloud-firestore/src/main/java/com/google/cloud/firestore/pipeline/stages/DeleteReturn.java @@ -16,7 +16,10 @@ package com.google.cloud.firestore.pipeline.stages; +import com.google.api.core.InternalApi; + /** Defines the return value options for a Delete pipeline stage. */ +@InternalApi public enum DeleteReturn { EMPTY("EMPTY"), DOCUMENT_ID("DOCUMENT_ID"); diff --git a/google-cloud-firestore/src/main/java/com/google/cloud/firestore/pipeline/stages/Insert.java b/google-cloud-firestore/src/main/java/com/google/cloud/firestore/pipeline/stages/Insert.java index 04555740a..98723cd28 100644 --- a/google-cloud-firestore/src/main/java/com/google/cloud/firestore/pipeline/stages/Insert.java +++ b/google-cloud-firestore/src/main/java/com/google/cloud/firestore/pipeline/stages/Insert.java @@ -47,12 +47,12 @@ public static Insert withCollection(CollectionReference target) { return new Insert(path.startsWith("/") ? path : "/" + path, InternalOptions.EMPTY); } - @BetaApi + @InternalApi public Insert withOptions(InsertOptions options) { return new Insert(path, this.options.adding(options)); } - @BetaApi + @InternalApi public Insert withReturns(InsertReturn returns) { return new Insert( path, this.options.with("returns", PipelineUtils.encodeValue(returns.getValue()))); diff --git a/google-cloud-firestore/src/main/java/com/google/cloud/firestore/pipeline/stages/InsertOptions.java b/google-cloud-firestore/src/main/java/com/google/cloud/firestore/pipeline/stages/InsertOptions.java index ee5c3f65e..cb6afc038 100644 --- a/google-cloud-firestore/src/main/java/com/google/cloud/firestore/pipeline/stages/InsertOptions.java +++ b/google-cloud-firestore/src/main/java/com/google/cloud/firestore/pipeline/stages/InsertOptions.java @@ -16,10 +16,10 @@ package com.google.cloud.firestore.pipeline.stages; -import com.google.api.core.BetaApi; +import com.google.api.core.InternalApi; /** Options for an Insert pipeline stage. */ -@BetaApi +@InternalApi public class InsertOptions extends WriteOptions { /** Creates a new, empty `InsertOptions` object. */ diff --git a/google-cloud-firestore/src/main/java/com/google/cloud/firestore/pipeline/stages/InsertReturn.java b/google-cloud-firestore/src/main/java/com/google/cloud/firestore/pipeline/stages/InsertReturn.java index bd44ec2f7..a0dac9ade 100644 --- a/google-cloud-firestore/src/main/java/com/google/cloud/firestore/pipeline/stages/InsertReturn.java +++ b/google-cloud-firestore/src/main/java/com/google/cloud/firestore/pipeline/stages/InsertReturn.java @@ -16,7 +16,10 @@ package com.google.cloud.firestore.pipeline.stages; +import com.google.api.core.InternalApi; + /** Defines the return value options for an Insert pipeline stage. */ +@InternalApi public enum InsertReturn { EMPTY("EMPTY"), DOCUMENT_ID("DOCUMENT_ID"); diff --git a/google-cloud-firestore/src/main/java/com/google/cloud/firestore/pipeline/stages/Update.java b/google-cloud-firestore/src/main/java/com/google/cloud/firestore/pipeline/stages/Update.java new file mode 100644 index 000000000..942d78e74 --- /dev/null +++ b/google-cloud-firestore/src/main/java/com/google/cloud/firestore/pipeline/stages/Update.java @@ -0,0 +1,78 @@ +/* + * Copyright 2024 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.cloud.firestore.pipeline.stages; + +import com.google.api.core.BetaApi; +import com.google.api.core.InternalApi; +import com.google.cloud.firestore.PipelineUtils; +import com.google.cloud.firestore.pipeline.expressions.Expression; +import com.google.cloud.firestore.pipeline.expressions.Selectable; +import com.google.firestore.v1.Value; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import javax.annotation.Nullable; + +@InternalApi +public final class Update extends Stage { + + @Nullable private final Selectable[] transformations; + + private Update(@Nullable Selectable[] transformations, InternalOptions options) { + super("update", options); + this.transformations = transformations; + } + + @BetaApi + public Update() { + this(null, InternalOptions.EMPTY); + } + + @InternalApi + public Update withOptions(UpdateOptions options) { + return new Update(transformations, this.options.adding(options)); + } + + @InternalApi + public Update withReturns(UpdateReturn returns) { + return new Update( + transformations, + this.options.with("returns", PipelineUtils.encodeValue(returns.getValue()))); + } + + @BetaApi + public Update withTransformations(Selectable... transformations) { + return new Update(transformations, this.options); + } + + @Override + Iterable toStageArgs() { + List args = new ArrayList<>(); + if (transformations != null && transformations.length > 0) { + Map map = PipelineUtils.selectablesToMap(transformations); + Map encodedMap = new HashMap<>(); + for (Map.Entry entry : map.entrySet()) { + encodedMap.put(entry.getKey(), PipelineUtils.encodeValue(entry.getValue())); + } + args.add(PipelineUtils.encodeValue(encodedMap)); + } else { + args.add(PipelineUtils.encodeValue(new HashMap())); + } + return args; + } +} diff --git a/google-cloud-firestore/src/main/java/com/google/cloud/firestore/pipeline/stages/UpdateOptions.java b/google-cloud-firestore/src/main/java/com/google/cloud/firestore/pipeline/stages/UpdateOptions.java new file mode 100644 index 000000000..6b68dae6e --- /dev/null +++ b/google-cloud-firestore/src/main/java/com/google/cloud/firestore/pipeline/stages/UpdateOptions.java @@ -0,0 +1,49 @@ +/* + * Copyright 2024 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.cloud.firestore.pipeline.stages; + +import com.google.api.core.InternalApi; + +/** Options for an Update pipeline stage. */ +@InternalApi +public class UpdateOptions extends WriteOptions { + + /** Creates a new, empty `UpdateOptions` object. */ + public UpdateOptions() { + super(InternalOptions.EMPTY); + } + + UpdateOptions(InternalOptions options) { + super(options); + } + + @Override + UpdateOptions self(InternalOptions options) { + return new UpdateOptions(options); + } + + /** + * Sets the conflict resolution strategy. + * + * @param conflictResolution The conflict resolution strategy. + * @return A new options object with the conflict resolution set. + */ + @InternalApi + public UpdateOptions withConflictResolution(ConflictResolution conflictResolution) { + return with("conflict_resolution", conflictResolution.getValue()); + } +} diff --git a/google-cloud-firestore/src/main/java/com/google/cloud/firestore/pipeline/stages/UpdateReturn.java b/google-cloud-firestore/src/main/java/com/google/cloud/firestore/pipeline/stages/UpdateReturn.java new file mode 100644 index 000000000..f70650a6b --- /dev/null +++ b/google-cloud-firestore/src/main/java/com/google/cloud/firestore/pipeline/stages/UpdateReturn.java @@ -0,0 +1,36 @@ +/* + * 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.pipeline.stages; + +import com.google.api.core.InternalApi; + +/** Defines the return value options for an Update pipeline stage. */ +@InternalApi +public enum UpdateReturn { + EMPTY("EMPTY"), + DOCUMENT_ID("DOCUMENT_ID"); + + private final String value; + + UpdateReturn(String value) { + this.value = value; + } + + public String getValue() { + return value; + } +} diff --git a/google-cloud-firestore/src/main/java/com/google/cloud/firestore/pipeline/stages/Upsert.java b/google-cloud-firestore/src/main/java/com/google/cloud/firestore/pipeline/stages/Upsert.java index 8dd9c1763..e4d33c875 100644 --- a/google-cloud-firestore/src/main/java/com/google/cloud/firestore/pipeline/stages/Upsert.java +++ b/google-cloud-firestore/src/main/java/com/google/cloud/firestore/pipeline/stages/Upsert.java @@ -47,12 +47,12 @@ public static Upsert withCollection(CollectionReference target) { return new Upsert(path.startsWith("/") ? path : "/" + path, InternalOptions.EMPTY); } - @BetaApi + @InternalApi public Upsert withOptions(UpsertOptions options) { return new Upsert(path, this.options.adding(options)); } - @BetaApi + @InternalApi public Upsert withReturns(UpsertReturn returns) { return new Upsert( path, this.options.with("returns", PipelineUtils.encodeValue(returns.getValue()))); diff --git a/google-cloud-firestore/src/main/java/com/google/cloud/firestore/pipeline/stages/UpsertOptions.java b/google-cloud-firestore/src/main/java/com/google/cloud/firestore/pipeline/stages/UpsertOptions.java index 98844e23e..e71b8a7fb 100644 --- a/google-cloud-firestore/src/main/java/com/google/cloud/firestore/pipeline/stages/UpsertOptions.java +++ b/google-cloud-firestore/src/main/java/com/google/cloud/firestore/pipeline/stages/UpsertOptions.java @@ -16,10 +16,10 @@ package com.google.cloud.firestore.pipeline.stages; -import com.google.api.core.BetaApi; +import com.google.api.core.InternalApi; /** Options for an Upsert pipeline stage. */ -@BetaApi +@InternalApi public class UpsertOptions extends WriteOptions { /** Creates a new, empty `UpsertOptions` object. */ @@ -42,7 +42,7 @@ UpsertOptions self(InternalOptions options) { * @param conflictResolution The conflict resolution strategy. * @return A new options object with the conflict resolution set. */ - @BetaApi + @InternalApi public UpsertOptions withConflictResolution(ConflictResolution conflictResolution) { return with("conflict_resolution", conflictResolution.getValue()); } diff --git a/google-cloud-firestore/src/main/java/com/google/cloud/firestore/pipeline/stages/UpsertReturn.java b/google-cloud-firestore/src/main/java/com/google/cloud/firestore/pipeline/stages/UpsertReturn.java index 4f7ee7089..7f799f534 100644 --- a/google-cloud-firestore/src/main/java/com/google/cloud/firestore/pipeline/stages/UpsertReturn.java +++ b/google-cloud-firestore/src/main/java/com/google/cloud/firestore/pipeline/stages/UpsertReturn.java @@ -16,7 +16,10 @@ package com.google.cloud.firestore.pipeline.stages; +import com.google.api.core.InternalApi; + /** Defines the return value options for an Upsert pipeline stage. */ +@InternalApi public enum UpsertReturn { EMPTY("EMPTY"), DOCUMENT_ID("DOCUMENT_ID"); diff --git a/google-cloud-firestore/src/test/java/com/google/cloud/firestore/it/ITPipelineTest.java b/google-cloud-firestore/src/test/java/com/google/cloud/firestore/it/ITPipelineTest.java index 39fc33e6c..bb9e6dd81 100644 --- a/google-cloud-firestore/src/test/java/com/google/cloud/firestore/it/ITPipelineTest.java +++ b/google-cloud-firestore/src/test/java/com/google/cloud/firestore/it/ITPipelineTest.java @@ -116,24 +116,14 @@ import com.google.cloud.firestore.pipeline.stages.AggregateOptions; import com.google.cloud.firestore.pipeline.stages.CollectionHints; import com.google.cloud.firestore.pipeline.stages.CollectionOptions; -import com.google.cloud.firestore.pipeline.stages.ConflictResolution; -import com.google.cloud.firestore.pipeline.stages.Delete; -import com.google.cloud.firestore.pipeline.stages.DeleteOptions; -import com.google.cloud.firestore.pipeline.stages.DeleteReturn; import com.google.cloud.firestore.pipeline.stages.ExplainOptions; import com.google.cloud.firestore.pipeline.stages.FindNearest; import com.google.cloud.firestore.pipeline.stages.FindNearestOptions; -import com.google.cloud.firestore.pipeline.stages.Insert; -import com.google.cloud.firestore.pipeline.stages.InsertOptions; -import com.google.cloud.firestore.pipeline.stages.InsertReturn; import com.google.cloud.firestore.pipeline.stages.PipelineExecuteOptions; import com.google.cloud.firestore.pipeline.stages.RawOptions; import com.google.cloud.firestore.pipeline.stages.RawStage; import com.google.cloud.firestore.pipeline.stages.Sample; import com.google.cloud.firestore.pipeline.stages.UnnestOptions; -import com.google.cloud.firestore.pipeline.stages.Upsert; -import com.google.cloud.firestore.pipeline.stages.UpsertOptions; -import com.google.cloud.firestore.pipeline.stages.UpsertReturn; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; import com.google.common.collect.Lists; @@ -3320,79 +3310,6 @@ public void testUnion() throws Exception { assertThat(results).hasSize(22); } - @Test - public void testDelete() throws Exception { - firestore - .pipeline() - .collection(collection) - .where(equal("title", "The Hitchhiker's Guide to the Galaxy")) - .delete() - .execute() - .get(); - - firestore - .pipeline() - .collection(collection) - .where(equal("title", "The Hitchhiker's Guide to the Galaxy")) - .delete( - new Delete().withReturns(DeleteReturn.DOCUMENT_ID), - new DeleteOptions().withTransactional(true)) - .execute() - .get(); - } - - @Test - public void testUpsert() throws Exception { - firestore - .pipeline() - .collection(collection) - .where(equal("title", "The Hitchhiker's Guide to the Galaxy")) - .upsert() - .execute() - .get(); - - firestore - .pipeline() - .collection(collection) - .where(equal("title", "The Hitchhiker's Guide to the Galaxy")) - .upsert(collection) - .execute() - .get(); - - firestore - .pipeline() - .collection(collection) - .where(equal("title", "The Hitchhiker's Guide to the Galaxy")) - .upsert( - new Upsert().withReturns(UpsertReturn.DOCUMENT_ID), - new UpsertOptions() - .withConflictResolution(ConflictResolution.MERGE) - .withTransactional(true)) - .execute() - .get(); - } - - @Test - public void testInsert() throws Exception { - firestore - .pipeline() - .collection(collection) - .where(equal("title", "The Hitchhiker's Guide to the Galaxy")) - .insert(collection) - .execute() - .get(); - - firestore - .pipeline() - .collection(collection) - .where(equal("title", "The Hitchhiker's Guide to the Galaxy")) - .insert( - new Insert().withReturns(InsertReturn.DOCUMENT_ID), - new InsertOptions().withTransactional(true)) - .execute() - .get(); - } - @Test public void testUnnest() throws Exception { List results = @@ -3856,4 +3773,63 @@ public void disallowDuplicateAliasesAcrossStages() { }); assertThat(exception).hasMessageThat().contains("Duplicate alias or field name"); } + + @Test + public void testDeleteStage() throws Exception { + CollectionReference dmlCol = testCollectionWithDocs(bookDocs); + List results = + firestore + .pipeline() + .collection(dmlCol.getPath()) + .where(equal(field("__name__").documentId(), "book1")) + .delete() + .execute() + .get() + .getResults(); + + assertThat(results).hasSize(1); + assertThat(results.get(0).getData().get("documents_modified")).isEqualTo(1L); + assertThat(dmlCol.document("book1").get().get().exists()).isFalse(); + } + + @Test + public void testUpdateStage() throws Exception { + CollectionReference dmlCol = testCollectionWithDocs(bookDocs); + List results = + firestore + .pipeline() + .collection(dmlCol.getPath()) + .where(equal(field("__name__").documentId(), "book3")) + .update(constant("baz").as("foo")) + .execute() + .get() + .getResults(); + + assertThat(results).hasSize(1); + assertThat(dmlCol.document("book3").get().get().get("foo")).isEqualTo("baz"); + } + + @Test + public void testUpsertStage() throws Exception { + CollectionReference dmlCol = testCollectionWithDocs(bookDocs); + firestore + .pipeline() + .collection(dmlCol.getPath()) + .where(equal(field("__name__").documentId(), "book1")) + .upsert() + .execute() + .get(); + } + + @Test + public void testInsertStage() throws Exception { + CollectionReference dmlCol = testCollectionWithDocs(bookDocs); + firestore + .pipeline() + .collection(dmlCol.getPath()) + .where(equal(field("__name__").documentId(), "book4")) + .insert(dmlCol) + .execute() + .get(); + } } From e162660561c46e3c64caeaa5ca2dcbfde621088c Mon Sep 17 00:00:00 2001 From: wu-hui Date: Thu, 26 Mar 2026 17:12:34 -0400 Subject: [PATCH 4/6] Add pipeline DML tests and fix update verification --- .../com/google/cloud/firestore/Pipeline.java | 48 +---- .../cloud/firestore/PipelineSource.java | 14 ++ .../firestore/pipeline/stages/Delete.java | 31 +-- .../pipeline/stages/DeleteOptions.java | 38 ---- .../firestore/pipeline/stages/Insert.java | 40 ++-- .../pipeline/stages/InsertOptions.java | 38 ---- .../firestore/pipeline/stages/Literals.java | 66 +++++++ .../firestore/pipeline/stages/Update.java | 12 -- .../pipeline/stages/UpdateOptions.java | 49 ----- .../firestore/pipeline/stages/Upsert.java | 48 ++--- .../pipeline/stages/UpsertOptions.java | 49 ----- .../pipeline/stages/WriteOptions.java | 39 ---- .../cloud/firestore/it/ITPipelineTest.java | 185 +++++++++++++++--- .../pipeline/stages/LiteralsTest.java | 79 ++++++++ 14 files changed, 368 insertions(+), 368 deletions(-) delete mode 100644 google-cloud-firestore/src/main/java/com/google/cloud/firestore/pipeline/stages/DeleteOptions.java delete mode 100644 google-cloud-firestore/src/main/java/com/google/cloud/firestore/pipeline/stages/InsertOptions.java create mode 100644 google-cloud-firestore/src/main/java/com/google/cloud/firestore/pipeline/stages/Literals.java delete mode 100644 google-cloud-firestore/src/main/java/com/google/cloud/firestore/pipeline/stages/UpdateOptions.java delete mode 100644 google-cloud-firestore/src/main/java/com/google/cloud/firestore/pipeline/stages/UpsertOptions.java delete mode 100644 google-cloud-firestore/src/main/java/com/google/cloud/firestore/pipeline/stages/WriteOptions.java create mode 100644 google-cloud-firestore/src/test/java/com/google/cloud/firestore/pipeline/stages/LiteralsTest.java diff --git a/google-cloud-firestore/src/main/java/com/google/cloud/firestore/Pipeline.java b/google-cloud-firestore/src/main/java/com/google/cloud/firestore/Pipeline.java index 72f416a3e..ef07f66fc 100644 --- a/google-cloud-firestore/src/main/java/com/google/cloud/firestore/Pipeline.java +++ b/google-cloud-firestore/src/main/java/com/google/cloud/firestore/Pipeline.java @@ -42,12 +42,10 @@ import com.google.cloud.firestore.pipeline.stages.Aggregate; import com.google.cloud.firestore.pipeline.stages.AggregateOptions; import com.google.cloud.firestore.pipeline.stages.Delete; -import com.google.cloud.firestore.pipeline.stages.DeleteOptions; import com.google.cloud.firestore.pipeline.stages.Distinct; import com.google.cloud.firestore.pipeline.stages.FindNearest; import com.google.cloud.firestore.pipeline.stages.FindNearestOptions; import com.google.cloud.firestore.pipeline.stages.Insert; -import com.google.cloud.firestore.pipeline.stages.InsertOptions; import com.google.cloud.firestore.pipeline.stages.Limit; import com.google.cloud.firestore.pipeline.stages.Offset; import com.google.cloud.firestore.pipeline.stages.PipelineExecuteOptions; @@ -63,9 +61,7 @@ import com.google.cloud.firestore.pipeline.stages.Unnest; import com.google.cloud.firestore.pipeline.stages.UnnestOptions; import com.google.cloud.firestore.pipeline.stages.Update; -import com.google.cloud.firestore.pipeline.stages.UpdateOptions; import com.google.cloud.firestore.pipeline.stages.Upsert; -import com.google.cloud.firestore.pipeline.stages.UpsertOptions; import com.google.cloud.firestore.pipeline.stages.Where; import com.google.cloud.firestore.telemetry.MetricsUtil.MetricsContext; import com.google.cloud.firestore.telemetry.TelemetryConstants; @@ -1013,29 +1009,6 @@ public Pipeline delete() { return append(new Delete()); } - /** - * Performs a delete operation on documents from previous stages. - * - * @param target The collection to delete from. - * @return A new {@code Pipeline} object with this stage appended to the stage list. - */ - @BetaApi - public Pipeline delete(CollectionReference target) { - return append(Delete.withCollection(target)); - } - - /** - * Performs a delete operation on documents from previous stages. - * - * @param deleteStage The {@code Delete} stage to append. - * @param options The {@code DeleteOptions} to apply to the stage. - * @return A new {@code Pipeline} object with this stage appended to the stage list. - */ - @InternalApi - public Pipeline delete(Delete deleteStage, DeleteOptions options) { - return append(deleteStage.withOptions(options)); - } - /** * Performs an upsert operation using documents from previous stages. * @@ -1054,19 +1027,18 @@ public Pipeline upsert() { */ @BetaApi public Pipeline upsert(CollectionReference target) { - return append(Upsert.withCollection(target)); + return append(new Upsert().withCollection(target)); } /** * Performs an upsert operation using documents from previous stages. * * @param upsertStage The {@code Upsert} stage to append. - * @param options The {@code UpsertOptions} to apply to the stage. * @return A new {@code Pipeline} object with this stage appended to the stage list. */ @InternalApi - public Pipeline upsert(Upsert upsertStage, UpsertOptions options) { - return append(upsertStage.withOptions(options)); + public Pipeline upsert(Upsert upsertStage) { + return append(upsertStage); } /** @@ -1094,12 +1066,11 @@ public Pipeline update(Selectable... transformations) { * Performs an update operation using documents from previous stages. * * @param update The {@code Update} stage to append. - * @param options The {@code UpdateOptions} to apply to the stage. * @return A new {@code Pipeline} object with this stage appended to the stage list. */ @InternalApi - public Pipeline update(Update updateStage, UpdateOptions options) { - return append(updateStage.withOptions(options)); + public Pipeline update(Update update) { + return append(update); } /** @@ -1120,19 +1091,18 @@ public Pipeline insert() { */ @BetaApi public Pipeline insert(CollectionReference target) { - return append(Insert.withCollection(target)); + return append(new Insert().withCollection(target)); } /** * Performs an insert operation using documents from previous stages. * * @param insertStage The {@code Insert} stage to append. - * @param options The {@code InsertOptions} to apply to the stage. * @return A new {@code Pipeline} object with this stage appended to the stage list. */ @InternalApi - public Pipeline insert(Insert insertStage, InsertOptions options) { - return append(insertStage.withOptions(options)); + public Pipeline insert(Insert insertStage) { + return append(insertStage); } /** @@ -1476,7 +1446,7 @@ public void onComplete() { } }; - logger.log(Level.FINEST, "Sending pipeline request: " + request.getStructuredPipeline()); + logger.log(Level.WARNING, "Sending pipeline request: " + request.getStructuredPipeline()); rpcContext.streamRequest(request, observer, rpcContext.getClient().executePipelineCallable()); } diff --git a/google-cloud-firestore/src/main/java/com/google/cloud/firestore/PipelineSource.java b/google-cloud-firestore/src/main/java/com/google/cloud/firestore/PipelineSource.java index 207ddd7f8..507ddad0c 100644 --- a/google-cloud-firestore/src/main/java/com/google/cloud/firestore/PipelineSource.java +++ b/google-cloud-firestore/src/main/java/com/google/cloud/firestore/PipelineSource.java @@ -24,6 +24,7 @@ import com.google.cloud.firestore.pipeline.stages.CollectionOptions; import com.google.cloud.firestore.pipeline.stages.Database; import com.google.cloud.firestore.pipeline.stages.Documents; +import com.google.cloud.firestore.pipeline.stages.Literals; import com.google.common.base.Preconditions; import java.util.Arrays; import javax.annotation.Nonnull; @@ -157,6 +158,19 @@ public Pipeline documents(String... docs) { .toArray(DocumentReference[]::new))); } + /** + * Creates a new {@link Pipeline} that operates on a static set of documents + * represented as Maps. + * + * @param data The Maps representing documents to include in the pipeline. + * @return A new {@code Pipeline} instance with a literals source. + */ + @Nonnull + @BetaApi + public final Pipeline literals(java.util.Map... data) { + return new Pipeline(this.rpcContext, new Literals(data)); + } + /** * Creates a new {@link Pipeline} from the given {@link Query}. Under the hood, this will * translate the query semantics (order by document ID, etc.) to an equivalent pipeline. diff --git a/google-cloud-firestore/src/main/java/com/google/cloud/firestore/pipeline/stages/Delete.java b/google-cloud-firestore/src/main/java/com/google/cloud/firestore/pipeline/stages/Delete.java index b72e89e86..25e3d2ab5 100644 --- a/google-cloud-firestore/src/main/java/com/google/cloud/firestore/pipeline/stages/Delete.java +++ b/google-cloud-firestore/src/main/java/com/google/cloud/firestore/pipeline/stages/Delete.java @@ -27,42 +27,17 @@ @InternalApi public final class Delete extends Stage { - - @Nullable private final String path; - - private Delete(@Nullable String path, InternalOptions options) { + private Delete(InternalOptions options) { super("delete", options); - this.path = path; } @BetaApi public Delete() { - this(null, InternalOptions.EMPTY); - } - - @BetaApi - public static Delete withCollection(CollectionReference target) { - String path = target.getPath(); - return new Delete(path.startsWith("/") ? path : "/" + path, InternalOptions.EMPTY); - } - - @InternalApi - public Delete withOptions(DeleteOptions options) { - return new Delete(path, this.options.adding(options)); - } - - @InternalApi - public Delete withReturns(DeleteReturn returns) { - return new Delete( - path, this.options.with("returns", PipelineUtils.encodeValue(returns.getValue()))); + this(InternalOptions.EMPTY); } @Override Iterable toStageArgs() { - List args = new ArrayList<>(); - if (path != null) { - args.add(Value.newBuilder().setReferenceValue(path).build()); - } - return args; + return new ArrayList<>(); } } diff --git a/google-cloud-firestore/src/main/java/com/google/cloud/firestore/pipeline/stages/DeleteOptions.java b/google-cloud-firestore/src/main/java/com/google/cloud/firestore/pipeline/stages/DeleteOptions.java deleted file mode 100644 index 356212920..000000000 --- a/google-cloud-firestore/src/main/java/com/google/cloud/firestore/pipeline/stages/DeleteOptions.java +++ /dev/null @@ -1,38 +0,0 @@ -/* - * Copyright 2024 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.google.cloud.firestore.pipeline.stages; - -import com.google.api.core.InternalApi; - -/** Options for a Delete pipeline stage. */ -@InternalApi -public class DeleteOptions extends WriteOptions { - - /** Creates a new, empty `DeleteOptions` object. */ - public DeleteOptions() { - super(InternalOptions.EMPTY); - } - - DeleteOptions(InternalOptions options) { - super(options); - } - - @Override - DeleteOptions self(InternalOptions options) { - return new DeleteOptions(options); - } -} diff --git a/google-cloud-firestore/src/main/java/com/google/cloud/firestore/pipeline/stages/Insert.java b/google-cloud-firestore/src/main/java/com/google/cloud/firestore/pipeline/stages/Insert.java index 98723cd28..f9367a470 100644 --- a/google-cloud-firestore/src/main/java/com/google/cloud/firestore/pipeline/stages/Insert.java +++ b/google-cloud-firestore/src/main/java/com/google/cloud/firestore/pipeline/stages/Insert.java @@ -20,59 +20,41 @@ import com.google.api.core.InternalApi; import com.google.cloud.firestore.CollectionReference; import com.google.cloud.firestore.PipelineUtils; +import com.google.cloud.firestore.pipeline.expressions.Expression; import com.google.cloud.firestore.pipeline.expressions.Selectable; import com.google.firestore.v1.Value; import java.util.ArrayList; import java.util.List; import javax.annotation.Nullable; -@InternalApi +@BetaApi public final class Insert extends Stage { - @Nullable private final String path; - - private Insert(@Nullable String path, InternalOptions options) { + private Insert(InternalOptions options) { super("insert", options); - this.path = path; } @BetaApi public Insert() { - this(null, InternalOptions.EMPTY); + this(InternalOptions.EMPTY); } @BetaApi - public static Insert withCollection(CollectionReference target) { + public Insert withCollection(CollectionReference target) { String path = target.getPath(); - return new Insert(path.startsWith("/") ? path : "/" + path, InternalOptions.EMPTY); - } - - @InternalApi - public Insert withOptions(InsertOptions options) { - return new Insert(path, this.options.adding(options)); - } - - @InternalApi - public Insert withReturns(InsertReturn returns) { + String normalizedPath = path.startsWith("/") ? path : "/" + path; return new Insert( - path, this.options.with("returns", PipelineUtils.encodeValue(returns.getValue()))); + this.options.with( + "collection", Value.newBuilder().setReferenceValue(normalizedPath).build())); } @BetaApi - public Insert withTransformations(Selectable... transformations) { - return new Insert( - path, - this.options.with( - "transformations", - PipelineUtils.encodeValue(PipelineUtils.selectablesToMap(transformations)))); + public Insert withIdExpression(Expression idExpr) { + return new Insert(this.options.with("document_id", PipelineUtils.encodeValue(idExpr))); } @Override Iterable toStageArgs() { - List args = new ArrayList<>(); - if (path != null) { - args.add(Value.newBuilder().setReferenceValue(path).build()); - } - return args; + return java.util.Collections.emptyList(); } } diff --git a/google-cloud-firestore/src/main/java/com/google/cloud/firestore/pipeline/stages/InsertOptions.java b/google-cloud-firestore/src/main/java/com/google/cloud/firestore/pipeline/stages/InsertOptions.java deleted file mode 100644 index cb6afc038..000000000 --- a/google-cloud-firestore/src/main/java/com/google/cloud/firestore/pipeline/stages/InsertOptions.java +++ /dev/null @@ -1,38 +0,0 @@ -/* - * Copyright 2024 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.google.cloud.firestore.pipeline.stages; - -import com.google.api.core.InternalApi; - -/** Options for an Insert pipeline stage. */ -@InternalApi -public class InsertOptions extends WriteOptions { - - /** Creates a new, empty `InsertOptions` object. */ - public InsertOptions() { - super(InternalOptions.EMPTY); - } - - InsertOptions(InternalOptions options) { - super(options); - } - - @Override - InsertOptions self(InternalOptions options) { - return new InsertOptions(options); - } -} diff --git a/google-cloud-firestore/src/main/java/com/google/cloud/firestore/pipeline/stages/Literals.java b/google-cloud-firestore/src/main/java/com/google/cloud/firestore/pipeline/stages/Literals.java new file mode 100644 index 000000000..17463b6b2 --- /dev/null +++ b/google-cloud-firestore/src/main/java/com/google/cloud/firestore/pipeline/stages/Literals.java @@ -0,0 +1,66 @@ +/* + * 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.pipeline.stages; + +import com.google.api.core.BetaApi; +import com.google.api.core.InternalApi; +import com.google.cloud.firestore.PipelineUtils; +import com.google.firestore.v1.Value; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.Map; + +@InternalApi +public final class Literals extends Stage { + + private final List> data; + + @BetaApi + public Literals(Map... data) { + super("literals", InternalOptions.EMPTY); + this.data = Arrays.asList(data); + } + + @Override + Iterable toStageArgs() { + List args = new ArrayList<>(); + for (Map map : data) { + args.add(encodeLiteralMap(map)); + } + return args; + } + + private Value encodeLiteralMap(Map map) { + com.google.firestore.v1.MapValue.Builder mapValue = com.google.firestore.v1.MapValue.newBuilder(); + for (Map.Entry entry : map.entrySet()) { + String key = String.valueOf(entry.getKey()); + Object v = entry.getValue(); + if (v instanceof com.google.cloud.firestore.pipeline.expressions.Expression) { + mapValue.putFields( + key, + PipelineUtils.encodeValue( + (com.google.cloud.firestore.pipeline.expressions.Expression) v)); + } else if (v instanceof Map) { + mapValue.putFields(key, encodeLiteralMap((Map) v)); + } else { + mapValue.putFields(key, PipelineUtils.encodeValue(v)); + } + } + return Value.newBuilder().setMapValue(mapValue.build()).build(); + } +} diff --git a/google-cloud-firestore/src/main/java/com/google/cloud/firestore/pipeline/stages/Update.java b/google-cloud-firestore/src/main/java/com/google/cloud/firestore/pipeline/stages/Update.java index 942d78e74..dad7d7dca 100644 --- a/google-cloud-firestore/src/main/java/com/google/cloud/firestore/pipeline/stages/Update.java +++ b/google-cloud-firestore/src/main/java/com/google/cloud/firestore/pipeline/stages/Update.java @@ -43,18 +43,6 @@ public Update() { this(null, InternalOptions.EMPTY); } - @InternalApi - public Update withOptions(UpdateOptions options) { - return new Update(transformations, this.options.adding(options)); - } - - @InternalApi - public Update withReturns(UpdateReturn returns) { - return new Update( - transformations, - this.options.with("returns", PipelineUtils.encodeValue(returns.getValue()))); - } - @BetaApi public Update withTransformations(Selectable... transformations) { return new Update(transformations, this.options); diff --git a/google-cloud-firestore/src/main/java/com/google/cloud/firestore/pipeline/stages/UpdateOptions.java b/google-cloud-firestore/src/main/java/com/google/cloud/firestore/pipeline/stages/UpdateOptions.java deleted file mode 100644 index 6b68dae6e..000000000 --- a/google-cloud-firestore/src/main/java/com/google/cloud/firestore/pipeline/stages/UpdateOptions.java +++ /dev/null @@ -1,49 +0,0 @@ -/* - * Copyright 2024 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.google.cloud.firestore.pipeline.stages; - -import com.google.api.core.InternalApi; - -/** Options for an Update pipeline stage. */ -@InternalApi -public class UpdateOptions extends WriteOptions { - - /** Creates a new, empty `UpdateOptions` object. */ - public UpdateOptions() { - super(InternalOptions.EMPTY); - } - - UpdateOptions(InternalOptions options) { - super(options); - } - - @Override - UpdateOptions self(InternalOptions options) { - return new UpdateOptions(options); - } - - /** - * Sets the conflict resolution strategy. - * - * @param conflictResolution The conflict resolution strategy. - * @return A new options object with the conflict resolution set. - */ - @InternalApi - public UpdateOptions withConflictResolution(ConflictResolution conflictResolution) { - return with("conflict_resolution", conflictResolution.getValue()); - } -} diff --git a/google-cloud-firestore/src/main/java/com/google/cloud/firestore/pipeline/stages/Upsert.java b/google-cloud-firestore/src/main/java/com/google/cloud/firestore/pipeline/stages/Upsert.java index e4d33c875..ebc7c3d28 100644 --- a/google-cloud-firestore/src/main/java/com/google/cloud/firestore/pipeline/stages/Upsert.java +++ b/google-cloud-firestore/src/main/java/com/google/cloud/firestore/pipeline/stages/Upsert.java @@ -20,20 +20,23 @@ import com.google.api.core.InternalApi; import com.google.cloud.firestore.CollectionReference; import com.google.cloud.firestore.PipelineUtils; +import com.google.cloud.firestore.pipeline.expressions.Expression; import com.google.cloud.firestore.pipeline.expressions.Selectable; import com.google.firestore.v1.Value; import java.util.ArrayList; +import java.util.HashMap; import java.util.List; +import java.util.Map; import javax.annotation.Nullable; -@InternalApi +@BetaApi public final class Upsert extends Stage { - @Nullable private final String path; + @Nullable private final Selectable[] transformations; - private Upsert(@Nullable String path, InternalOptions options) { + private Upsert(@Nullable Selectable[] transformations, InternalOptions options) { super("upsert", options); - this.path = path; + this.transformations = transformations; } @BetaApi @@ -42,36 +45,37 @@ public Upsert() { } @BetaApi - public static Upsert withCollection(CollectionReference target) { + public Upsert withCollection(CollectionReference target) { String path = target.getPath(); - return new Upsert(path.startsWith("/") ? path : "/" + path, InternalOptions.EMPTY); - } - - @InternalApi - public Upsert withOptions(UpsertOptions options) { - return new Upsert(path, this.options.adding(options)); + String normalizedPath = path.startsWith("/") ? path : "/" + path; + return new Upsert( + this.transformations, + this.options.with( + "collection", Value.newBuilder().setReferenceValue(normalizedPath).build())); } - @InternalApi - public Upsert withReturns(UpsertReturn returns) { - return new Upsert( - path, this.options.with("returns", PipelineUtils.encodeValue(returns.getValue()))); + @BetaApi + public Upsert withIdExpression(Expression idExpr) { + return new Upsert(this.transformations, this.options.with("document_id", PipelineUtils.encodeValue(idExpr))); } @BetaApi public Upsert withTransformations(Selectable... transformations) { - return new Upsert( - path, - this.options.with( - "transformations", - PipelineUtils.encodeValue(PipelineUtils.selectablesToMap(transformations)))); + return new Upsert(transformations, this.options); } @Override Iterable toStageArgs() { List args = new ArrayList<>(); - if (path != null) { - args.add(Value.newBuilder().setReferenceValue(path).build()); + if (transformations != null && transformations.length > 0) { + Map map = PipelineUtils.selectablesToMap(transformations); + Map encodedMap = new HashMap<>(); + for (Map.Entry entry : map.entrySet()) { + encodedMap.put(entry.getKey(), PipelineUtils.encodeValue(entry.getValue())); + } + args.add(PipelineUtils.encodeValue(encodedMap)); + } else { + args.add(PipelineUtils.encodeValue(new HashMap())); } return args; } diff --git a/google-cloud-firestore/src/main/java/com/google/cloud/firestore/pipeline/stages/UpsertOptions.java b/google-cloud-firestore/src/main/java/com/google/cloud/firestore/pipeline/stages/UpsertOptions.java deleted file mode 100644 index e71b8a7fb..000000000 --- a/google-cloud-firestore/src/main/java/com/google/cloud/firestore/pipeline/stages/UpsertOptions.java +++ /dev/null @@ -1,49 +0,0 @@ -/* - * Copyright 2024 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.google.cloud.firestore.pipeline.stages; - -import com.google.api.core.InternalApi; - -/** Options for an Upsert pipeline stage. */ -@InternalApi -public class UpsertOptions extends WriteOptions { - - /** Creates a new, empty `UpsertOptions` object. */ - public UpsertOptions() { - super(InternalOptions.EMPTY); - } - - UpsertOptions(InternalOptions options) { - super(options); - } - - @Override - UpsertOptions self(InternalOptions options) { - return new UpsertOptions(options); - } - - /** - * Sets the conflict resolution strategy. - * - * @param conflictResolution The conflict resolution strategy. - * @return A new options object with the conflict resolution set. - */ - @InternalApi - public UpsertOptions withConflictResolution(ConflictResolution conflictResolution) { - return with("conflict_resolution", conflictResolution.getValue()); - } -} diff --git a/google-cloud-firestore/src/main/java/com/google/cloud/firestore/pipeline/stages/WriteOptions.java b/google-cloud-firestore/src/main/java/com/google/cloud/firestore/pipeline/stages/WriteOptions.java deleted file mode 100644 index ac4dd92c1..000000000 --- a/google-cloud-firestore/src/main/java/com/google/cloud/firestore/pipeline/stages/WriteOptions.java +++ /dev/null @@ -1,39 +0,0 @@ -/* - * Copyright 2024 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.google.cloud.firestore.pipeline.stages; - -import com.google.api.core.BetaApi; - -/** Options for write stages in a pipeline. */ -@BetaApi -public abstract class WriteOptions> extends AbstractOptions { - - WriteOptions(InternalOptions options) { - super(options); - } - - /** - * Sets the transactional option. - * - * @param transactional Whether the operation should be transactional. - * @return A new options object with the transactional option set. - */ - @BetaApi - public T withTransactional(boolean transactional) { - return with("transactional", transactional); - } -} diff --git a/google-cloud-firestore/src/test/java/com/google/cloud/firestore/it/ITPipelineTest.java b/google-cloud-firestore/src/test/java/com/google/cloud/firestore/it/ITPipelineTest.java index bb9e6dd81..4c689e36c 100644 --- a/google-cloud-firestore/src/test/java/com/google/cloud/firestore/it/ITPipelineTest.java +++ b/google-cloud-firestore/src/test/java/com/google/cloud/firestore/it/ITPipelineTest.java @@ -102,6 +102,7 @@ import com.google.cloud.Timestamp; import com.google.cloud.firestore.Blob; import com.google.cloud.firestore.CollectionReference; +import com.google.cloud.firestore.DocumentSnapshot; import com.google.cloud.firestore.Firestore; import com.google.cloud.firestore.FirestoreOptions; import com.google.cloud.firestore.GeoPoint; @@ -119,11 +120,13 @@ import com.google.cloud.firestore.pipeline.stages.ExplainOptions; import com.google.cloud.firestore.pipeline.stages.FindNearest; import com.google.cloud.firestore.pipeline.stages.FindNearestOptions; +import com.google.cloud.firestore.pipeline.stages.Insert; import com.google.cloud.firestore.pipeline.stages.PipelineExecuteOptions; import com.google.cloud.firestore.pipeline.stages.RawOptions; import com.google.cloud.firestore.pipeline.stages.RawStage; import com.google.cloud.firestore.pipeline.stages.Sample; import com.google.cloud.firestore.pipeline.stages.UnnestOptions; +import com.google.cloud.firestore.pipeline.stages.Upsert; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; import com.google.common.collect.Lists; @@ -3775,61 +3778,193 @@ public void disallowDuplicateAliasesAcrossStages() { } @Test - public void testDeleteStage() throws Exception { + public void testInsertWithIdExpression() throws Exception { + CollectionReference targetCol = firestore.collection(LocalFirestoreHelper.autoId()); + + java.util.Map book = new java.util.HashMap<>(); + book.put("title", "The Hitchhiker's Guide to the Galaxy"); + book.put("doc_id", "book1"); + + firestore + .pipeline() + .literals(book) + .insert(new Insert().withCollection(targetCol).withIdExpression(field("doc_id"))) + .execute() + .get(); + + DocumentSnapshot doc = targetCol.document("book1").get().get(); + assertThat(doc.exists()).isTrue(); + assertThat(doc.get("title")).isEqualTo("The Hitchhiker's Guide to the Galaxy"); + } + + @Test + public void testInsertToExistingDocumentFails() throws Exception { + CollectionReference targetCol = firestore.collection(LocalFirestoreHelper.autoId()); + targetCol.document("book1").set(map("title", "Existing")).get(); + + java.util.Map book = new java.util.HashMap<>(); + book.put("title", "Duplicate"); + book.put("__name__", targetCol.document("book1")); + + assertThrows( + ExecutionException.class, + () -> { + firestore + .pipeline() + .literals(book) + .insert(targetCol) + .execute() + .get(); + }); + } + + @Test + public void testDeleteMultipleDocuments() throws Exception { CollectionReference dmlCol = testCollectionWithDocs(bookDocs); List results = firestore .pipeline() .collection(dmlCol.getPath()) - .where(equal(field("__name__").documentId(), "book1")) + .where(equal(field("genre"), "Science Fiction")) .delete() .execute() .get() .getResults(); assertThat(results).hasSize(1); - assertThat(results.get(0).getData().get("documents_modified")).isEqualTo(1L); + assertThat(results.get(0).getData().get("documents_modified")).isEqualTo(2L); assertThat(dmlCol.document("book1").get().get().exists()).isFalse(); + assertThat(dmlCol.document("book10").get().get().exists()).isFalse(); } @Test - public void testUpdateStage() throws Exception { + public void testUpdateMultipleDocuments() throws Exception { CollectionReference dmlCol = testCollectionWithDocs(bookDocs); List results = firestore .pipeline() .collection(dmlCol.getPath()) - .where(equal(field("__name__").documentId(), "book3")) - .update(constant("baz").as("foo")) + .where(equal(field("genre"), "Science Fiction")) + .update(constant("Updated").as("status")) .execute() .get() .getResults(); assertThat(results).hasSize(1); - assertThat(dmlCol.document("book3").get().get().get("foo")).isEqualTo("baz"); + assertThat(results.get(0).getData().get("documents_modified")).isEqualTo(2L); + assertThat(dmlCol.document("book1").get().get().get("status")).isEqualTo("Updated"); + assertThat(dmlCol.document("book10").get().get().get("status")).isEqualTo("Updated"); } @Test - public void testUpsertStage() throws Exception { + public void testUpdateWithExpressions() throws Exception { CollectionReference dmlCol = testCollectionWithDocs(bookDocs); - firestore - .pipeline() - .collection(dmlCol.getPath()) - .where(equal(field("__name__").documentId(), "book1")) - .upsert() - .execute() - .get(); - } + List results = + firestore + .pipeline() + .collection(dmlCol.getPath()) + .where(equal(field("__name__").documentId(), "book1")) + .update(Expression.add(field("rating"), constant(1.0)).as("rating")) + .execute() + .get() + .getResults(); - @Test - public void testInsertStage() throws Exception { + assertThat(results).hasSize(1); + DocumentSnapshot doc = dmlCol.document("book1").get().get(); + assertThat(doc.get("rating")).isEqualTo(5.2); +} + +@Test +public void testUpdateNonExistingDocumentModifiesZeroDocuments() throws Exception { + CollectionReference dmlCol = firestore.collection(LocalFirestoreHelper.autoId()); + + java.util.Map book = new java.util.HashMap<>(); + book.put("title", "Non Existing"); + book.put("__name__", dmlCol.document("nonExisting")); + + List results = + firestore + .pipeline() + .literals(book) + .update() + .execute() + .get() + .getResults(); + + assertThat(results).hasSize(1); + assertThat(results.get(0).getData().get("documents_modified")).isEqualTo(0L); +} + +@Test +public void testUpsertInPlaceReadingFromCollection() throws Exception { + assumeFalse( + "Transactions are not supported against the emulator.", + isRunningAgainstFirestoreEmulator(firestore)); CollectionReference dmlCol = testCollectionWithDocs(bookDocs); - firestore - .pipeline() - .collection(dmlCol.getPath()) - .where(equal(field("__name__").documentId(), "book4")) - .insert(dmlCol) - .execute() - .get(); + + firestore.runTransaction( + transaction -> { + transaction.execute( + firestore + .pipeline() + .collection(dmlCol.getPath()) + .where(equal(field("__name__").documentId(), "book3")) + .upsert(new Upsert().withTransformations(constant("Updated").as("status"))) + ).get(); + return "done"; + } + ).get(); + + DocumentSnapshot doc = dmlCol.document("book3").get().get(); + assertThat(doc.get("status")).isEqualTo("Updated"); +} + +@Test +public void testUpsertToNewCollectionReadingFromCollection() throws Exception { + assumeFalse( + "Transactions are not supported against the emulator.", + isRunningAgainstFirestoreEmulator(firestore)); + CollectionReference dmlCol = testCollectionWithDocs(bookDocs); + CollectionReference targetCol = firestore.collection(LocalFirestoreHelper.autoId()); + + firestore.runTransaction( + transaction -> { + transaction.execute( + firestore + .pipeline() + .collection(dmlCol.getPath()) + .where(equal(field("__name__").documentId(), "book1")) + .upsert(new Upsert().withCollection(targetCol).withTransformations(constant("Copied").as("status"))) + ).get(); + return "done"; + } + ).get(); + + DocumentSnapshot doc = targetCol.document("book1").get().get(); + assertThat(doc.exists()).isTrue(); + assertThat(doc.get("status")).isEqualTo("Copied"); + assertThat(dmlCol.document("book1").get().get().exists()).isTrue(); +} + + + +@Test +public void testLiteralsStage() throws Exception { + java.util.Map data1 = new java.util.HashMap<>(); + data1.put("foo", "bar"); + java.util.Map data2 = new java.util.HashMap<>(); + data2.put("baz", "qux"); + + List results = firestore + .pipeline() + .literals(data1, data2) + .execute() + .get() + .getResults(); + + assertThat(results).hasSize(2); + assertThat(results.get(0).getData()).isEqualTo(data1); + assertThat(results.get(1).getData()).isEqualTo(data2); } } + diff --git a/google-cloud-firestore/src/test/java/com/google/cloud/firestore/pipeline/stages/LiteralsTest.java b/google-cloud-firestore/src/test/java/com/google/cloud/firestore/pipeline/stages/LiteralsTest.java new file mode 100644 index 000000000..affa30fa0 --- /dev/null +++ b/google-cloud-firestore/src/test/java/com/google/cloud/firestore/pipeline/stages/LiteralsTest.java @@ -0,0 +1,79 @@ +/* + * 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.pipeline.stages; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; + +import com.google.cloud.firestore.pipeline.expressions.Field; +import com.google.firestore.v1.Value; +import java.util.HashMap; +import java.util.Map; +import org.junit.Test; + +public class LiteralsTest { + + @Test + public void testToStageArgsWithConstant() { + Map map = new HashMap<>(); + map.put("title", "The Hitchhiker's Guide to the Galaxy"); + + Literals literals = new Literals(map); + Iterable args = literals.toStageArgs(); + + assertTrue(args.iterator().hasNext()); + Value value = args.iterator().next(); + assertTrue(value.hasMapValue()); + assertEquals( + "The Hitchhiker's Guide to the Galaxy", + value.getMapValue().getFieldsOrThrow("title").getStringValue()); + } + + @Test + public void testToStageArgsWithExpression() { + Map map = new HashMap<>(); + map.put("title", "The Hitchhiker's Guide to the Galaxy"); + map.put( + "message", + com.google.cloud.firestore.pipeline.expressions.Expression.concat( + Field.ofServerPath("user_name"), " is awesome")); + + Literals literals = new Literals(map); + Iterable args = literals.toStageArgs(); + + assertTrue(args.iterator().hasNext()); + Value value = args.iterator().next(); + assertTrue(value.hasMapValue()); + assertEquals( + "The Hitchhiker's Guide to the Galaxy", + value.getMapValue().getFieldsOrThrow("title").getStringValue()); + + Value messageValue = value.getMapValue().getFieldsOrThrow("message"); + assertTrue(messageValue.hasFunctionValue()); + com.google.firestore.v1.Function func = messageValue.getFunctionValue(); + assertEquals("concat", func.getName()); + assertEquals(2, func.getArgsCount()); + + Value arg0 = func.getArgs(0); + assertTrue(arg0.hasFieldReferenceValue()); + assertEquals("user_name", arg0.getFieldReferenceValue()); + + Value arg1 = func.getArgs(1); + assertTrue(arg1.hasStringValue()); + assertEquals(" is awesome", arg1.getStringValue()); + } +} From f1bba0132c99085b6e3e65e36c94fc80768ad274 Mon Sep 17 00:00:00 2001 From: wu-hui Date: Thu, 26 Mar 2026 17:18:58 -0400 Subject: [PATCH 5/6] Delete upsert() from Pipeline.java and tests --- .../com/google/cloud/firestore/Pipeline.java | 168 +++++++--- .../cloud/firestore/PipelineSource.java | 17 +- .../firestore/pipeline/stages/Delete.java | 6 +- .../firestore/pipeline/stages/Insert.java | 7 +- .../firestore/pipeline/stages/Literals.java | 3 +- .../firestore/pipeline/stages/Update.java | 2 +- .../firestore/pipeline/stages/Upsert.java | 4 +- .../cloud/firestore/it/ITPipelineTest.java | 298 ++++++++++-------- 8 files changed, 310 insertions(+), 195 deletions(-) diff --git a/google-cloud-firestore/src/main/java/com/google/cloud/firestore/Pipeline.java b/google-cloud-firestore/src/main/java/com/google/cloud/firestore/Pipeline.java index ef07f66fc..bd8683868 100644 --- a/google-cloud-firestore/src/main/java/com/google/cloud/firestore/Pipeline.java +++ b/google-cloud-firestore/src/main/java/com/google/cloud/firestore/Pipeline.java @@ -61,7 +61,6 @@ import com.google.cloud.firestore.pipeline.stages.Unnest; import com.google.cloud.firestore.pipeline.stages.UnnestOptions; import com.google.cloud.firestore.pipeline.stages.Update; -import com.google.cloud.firestore.pipeline.stages.Upsert; import com.google.cloud.firestore.pipeline.stages.Where; import com.google.cloud.firestore.telemetry.MetricsUtil.MetricsContext; import com.google.cloud.firestore.telemetry.TelemetryConstants; @@ -1002,6 +1001,18 @@ public Pipeline unnest(Selectable field, UnnestOptions options) { /** * Performs a delete operation on documents from previous stages. * + *

Example: + * + *

{@code
+   * // Delete all documents in the "logs" collection where "status" is "archived"
+   * firestore.pipeline()
+   *     .collection("logs")
+   *     .where(field("status").equal("archived"))
+   *     .delete()
+   *     .execute()
+   *     .get();
+   * }
+ * * @return A new {@code Pipeline} object with this stage appended to the stage list. */ @BetaApi @@ -1010,39 +1021,36 @@ public Pipeline delete() { } /** - * Performs an upsert operation using documents from previous stages. + * Performs an update operation using documents from previous stages. * - * @return A new {@code Pipeline} object with this stage appended to the stage list. - */ - @BetaApi - public Pipeline upsert() { - return append(new Upsert()); - } - - /** - * Performs an upsert operation using documents from previous stages. + *

This method updates the documents in place based on the data flowing through the pipeline. + * To specify transformations, use {@link #update(Selectable...)}. * - * @param target The collection to upsert to. - * @return A new {@code Pipeline} object with this stage appended to the stage list. - */ - @BetaApi - public Pipeline upsert(CollectionReference target) { - return append(new Upsert().withCollection(target)); - } - - /** - * Performs an upsert operation using documents from previous stages. + *

Example 1: Update a collection's schema by adding a new field and removing an old one. * - * @param upsertStage The {@code Upsert} stage to append. - * @return A new {@code Pipeline} object with this stage appended to the stage list. - */ - @InternalApi - public Pipeline upsert(Upsert upsertStage) { - return append(upsertStage); - } - - /** - * Performs an update operation using documents from previous stages. + *

{@code
+   * firestore.pipeline()
+   *     .collection("books")
+   *     .addFields(constant("Fiction").as("genre"))
+   *     .removeFields("old_genre")
+   *     .update()
+   *     .execute()
+   *     .get();
+   * }
+ * + *

Example 2: Update documents in place with data from literals. + * + *

{@code
+   * Map updateData = new HashMap<>();
+   * updateData.put("__name__", firestore.collection("books").document("book1"));
+   * updateData.put("status", "Updated");
+   *
+   * firestore.pipeline()
+   *     .literals(updateData)
+   *     .update()
+   *     .execute()
+   *     .get();
+   * }
* * @return A new {@code Pipeline} object with this stage appended to the stage list. */ @@ -1052,7 +1060,20 @@ public Pipeline update() { } /** - * Performs an update operation using documents from previous stages. + * Performs an update operation using documents from previous stages with specified + * transformations. + * + *

Example: + * + *

{@code
+   * // Update the "status" field to "Discounted" for all books where price > 50
+   * firestore.pipeline()
+   *     .collection("books")
+   *     .where(field("price").greaterThan(50))
+   *     .update(constant("Discounted").as("status"))
+   *     .execute()
+   *     .get();
+   * }
* * @param transformations The transformations to apply. * @return A new {@code Pipeline} object with this stage appended to the stage list. @@ -1063,12 +1084,27 @@ public Pipeline update(Selectable... transformations) { } /** - * Performs an update operation using documents from previous stages. + * Performs an update operation using an {@link Update} stage. + * + *

This method allows you to use a pre-configured {@link Update} stage. + * + *

Example: + * + *

{@code
+   * Update updateStage = new Update().withTransformations(constant("Updated").as("status"));
+   *
+   * firestore.pipeline()
+   *     .collection("books")
+   *     .where(field("title").equal("The Hitchhiker's Guide to the Galaxy"))
+   *     .update(updateStage)
+   *     .execute()
+   *     .get();
+   * }
* * @param update The {@code Update} stage to append. * @return A new {@code Pipeline} object with this stage appended to the stage list. */ - @InternalApi + @BetaApi public Pipeline update(Update update) { return append(update); } @@ -1076,6 +1112,23 @@ public Pipeline update(Update update) { /** * Performs an insert operation using documents from previous stages. * + *

The documents must include a valid {@code __name__} field specifying the document reference + * to insert. If the document already exists, the operation will fail. + * + *

Example: + * + *

{@code
+   * Map book = new HashMap<>();
+   * book.put("__name__", firestore.collection("books").document("newBook"));
+   * book.put("title", "New Book");
+   *
+   * firestore.pipeline()
+   *     .literals(book)
+   *     .insert()
+   *     .execute()
+   *     .get();
+   * }
+ * * @return A new {@code Pipeline} object with this stage appended to the stage list. */ @BetaApi @@ -1084,7 +1137,26 @@ public Pipeline insert() { } /** - * Performs an insert operation using documents from previous stages. + * Performs an insert operation using documents from previous stages into a specified target + * collection. + * + *

If documents have an ID (or expression evaluation for ID), they will use it. Otherwise, + * auto-generated IDs will be used if applicable (depending on the source). + * + *

Example: + * + *

{@code
+   * CollectionReference backupCol = firestore.collection("books_backup");
+   *
+   * Map book = new HashMap<>();
+   * book.put("title", "New Book");
+   *
+   * firestore.pipeline()
+   *     .literals(book)
+   *     .insert(backupCol)
+   *     .execute()
+   *     .get();
+   * }
* * @param target The collection to insert to. * @return A new {@code Pipeline} object with this stage appended to the stage list. @@ -1095,12 +1167,32 @@ public Pipeline insert(CollectionReference target) { } /** - * Performs an insert operation using documents from previous stages. + * Performs an insert operation using an {@link Insert} stage. + * + *

This method allows you to use a pre-configured {@link Insert} stage. + * + *

Example: Use a pre-configured {@link Insert} stage with a target collection and ID + * expression, reading from literals. + * + *

{@code
+   * CollectionReference targetCol = firestore.collection("books_backup");
+   * Insert insertStage = new Insert().withCollection(targetCol).withIdExpression(field("custom_id"));
+   *
+   * Map book = new HashMap<>();
+   * book.put("custom_id", "book1");
+   * book.put("title", "Book 1");
+   *
+   * firestore.pipeline()
+   *     .literals(book)
+   *     .insert(insertStage)
+   *     .execute()
+   *     .get();
+   * }
* * @param insertStage The {@code Insert} stage to append. * @return A new {@code Pipeline} object with this stage appended to the stage list. */ - @InternalApi + @BetaApi public Pipeline insert(Insert insertStage) { return append(insertStage); } @@ -1446,7 +1538,7 @@ public void onComplete() { } }; - logger.log(Level.WARNING, "Sending pipeline request: " + request.getStructuredPipeline()); + logger.log(Level.FINEST, "Sending pipeline request: " + request.getStructuredPipeline()); rpcContext.streamRequest(request, observer, rpcContext.getClient().executePipelineCallable()); } diff --git a/google-cloud-firestore/src/main/java/com/google/cloud/firestore/PipelineSource.java b/google-cloud-firestore/src/main/java/com/google/cloud/firestore/PipelineSource.java index 507ddad0c..308782fa3 100644 --- a/google-cloud-firestore/src/main/java/com/google/cloud/firestore/PipelineSource.java +++ b/google-cloud-firestore/src/main/java/com/google/cloud/firestore/PipelineSource.java @@ -159,8 +159,21 @@ public Pipeline documents(String... docs) { } /** - * Creates a new {@link Pipeline} that operates on a static set of documents - * represented as Maps. + * Creates a new {@link Pipeline} that operates on a static set of documents represented as Maps. + * + *

Example: + * + *

{@code
+   * Map doc1 = new HashMap<>();
+   * doc1.put("title", "Book 1");
+   * Map doc2 = new HashMap<>();
+   * doc2.put("title", "Book 2");
+   *
+   * Snapshot snapshot = firestore.pipeline()
+   *     .literals(doc1, doc2)
+   *     .execute()
+   *     .get();
+   * }
* * @param data The Maps representing documents to include in the pipeline. * @return A new {@code Pipeline} instance with a literals source. diff --git a/google-cloud-firestore/src/main/java/com/google/cloud/firestore/pipeline/stages/Delete.java b/google-cloud-firestore/src/main/java/com/google/cloud/firestore/pipeline/stages/Delete.java index 25e3d2ab5..c288f0d42 100644 --- a/google-cloud-firestore/src/main/java/com/google/cloud/firestore/pipeline/stages/Delete.java +++ b/google-cloud-firestore/src/main/java/com/google/cloud/firestore/pipeline/stages/Delete.java @@ -1,5 +1,5 @@ /* - * Copyright 2024 Google LLC + * 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. @@ -18,12 +18,8 @@ import com.google.api.core.BetaApi; import com.google.api.core.InternalApi; -import com.google.cloud.firestore.CollectionReference; -import com.google.cloud.firestore.PipelineUtils; import com.google.firestore.v1.Value; import java.util.ArrayList; -import java.util.List; -import javax.annotation.Nullable; @InternalApi public final class Delete extends Stage { diff --git a/google-cloud-firestore/src/main/java/com/google/cloud/firestore/pipeline/stages/Insert.java b/google-cloud-firestore/src/main/java/com/google/cloud/firestore/pipeline/stages/Insert.java index f9367a470..21df157e8 100644 --- a/google-cloud-firestore/src/main/java/com/google/cloud/firestore/pipeline/stages/Insert.java +++ b/google-cloud-firestore/src/main/java/com/google/cloud/firestore/pipeline/stages/Insert.java @@ -1,5 +1,5 @@ /* - * Copyright 2024 Google LLC + * 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. @@ -17,15 +17,10 @@ package com.google.cloud.firestore.pipeline.stages; import com.google.api.core.BetaApi; -import com.google.api.core.InternalApi; import com.google.cloud.firestore.CollectionReference; import com.google.cloud.firestore.PipelineUtils; import com.google.cloud.firestore.pipeline.expressions.Expression; -import com.google.cloud.firestore.pipeline.expressions.Selectable; import com.google.firestore.v1.Value; -import java.util.ArrayList; -import java.util.List; -import javax.annotation.Nullable; @BetaApi public final class Insert extends Stage { diff --git a/google-cloud-firestore/src/main/java/com/google/cloud/firestore/pipeline/stages/Literals.java b/google-cloud-firestore/src/main/java/com/google/cloud/firestore/pipeline/stages/Literals.java index 17463b6b2..d3d5e5272 100644 --- a/google-cloud-firestore/src/main/java/com/google/cloud/firestore/pipeline/stages/Literals.java +++ b/google-cloud-firestore/src/main/java/com/google/cloud/firestore/pipeline/stages/Literals.java @@ -46,7 +46,8 @@ Iterable toStageArgs() { } private Value encodeLiteralMap(Map map) { - com.google.firestore.v1.MapValue.Builder mapValue = com.google.firestore.v1.MapValue.newBuilder(); + com.google.firestore.v1.MapValue.Builder mapValue = + com.google.firestore.v1.MapValue.newBuilder(); for (Map.Entry entry : map.entrySet()) { String key = String.valueOf(entry.getKey()); Object v = entry.getValue(); diff --git a/google-cloud-firestore/src/main/java/com/google/cloud/firestore/pipeline/stages/Update.java b/google-cloud-firestore/src/main/java/com/google/cloud/firestore/pipeline/stages/Update.java index dad7d7dca..6e125026c 100644 --- a/google-cloud-firestore/src/main/java/com/google/cloud/firestore/pipeline/stages/Update.java +++ b/google-cloud-firestore/src/main/java/com/google/cloud/firestore/pipeline/stages/Update.java @@ -1,5 +1,5 @@ /* - * Copyright 2024 Google LLC + * 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. diff --git a/google-cloud-firestore/src/main/java/com/google/cloud/firestore/pipeline/stages/Upsert.java b/google-cloud-firestore/src/main/java/com/google/cloud/firestore/pipeline/stages/Upsert.java index ebc7c3d28..f1f646ee4 100644 --- a/google-cloud-firestore/src/main/java/com/google/cloud/firestore/pipeline/stages/Upsert.java +++ b/google-cloud-firestore/src/main/java/com/google/cloud/firestore/pipeline/stages/Upsert.java @@ -17,7 +17,6 @@ package com.google.cloud.firestore.pipeline.stages; import com.google.api.core.BetaApi; -import com.google.api.core.InternalApi; import com.google.cloud.firestore.CollectionReference; import com.google.cloud.firestore.PipelineUtils; import com.google.cloud.firestore.pipeline.expressions.Expression; @@ -56,7 +55,8 @@ public Upsert withCollection(CollectionReference target) { @BetaApi public Upsert withIdExpression(Expression idExpr) { - return new Upsert(this.transformations, this.options.with("document_id", PipelineUtils.encodeValue(idExpr))); + return new Upsert( + this.transformations, this.options.with("document_id", PipelineUtils.encodeValue(idExpr))); } @BetaApi diff --git a/google-cloud-firestore/src/test/java/com/google/cloud/firestore/it/ITPipelineTest.java b/google-cloud-firestore/src/test/java/com/google/cloud/firestore/it/ITPipelineTest.java index 4c689e36c..20ade73d6 100644 --- a/google-cloud-firestore/src/test/java/com/google/cloud/firestore/it/ITPipelineTest.java +++ b/google-cloud-firestore/src/test/java/com/google/cloud/firestore/it/ITPipelineTest.java @@ -67,6 +67,7 @@ import static com.google.cloud.firestore.pipeline.expressions.Expression.logicalMinimum; import static com.google.cloud.firestore.pipeline.expressions.Expression.mapMerge; import static com.google.cloud.firestore.pipeline.expressions.Expression.mapRemove; +import static com.google.cloud.firestore.pipeline.expressions.Expression.multiply; import static com.google.cloud.firestore.pipeline.expressions.Expression.nor; import static com.google.cloud.firestore.pipeline.expressions.Expression.notEqual; import static com.google.cloud.firestore.pipeline.expressions.Expression.nullValue; @@ -126,7 +127,6 @@ import com.google.cloud.firestore.pipeline.stages.RawStage; import com.google.cloud.firestore.pipeline.stages.Sample; import com.google.cloud.firestore.pipeline.stages.UnnestOptions; -import com.google.cloud.firestore.pipeline.stages.Upsert; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; import com.google.common.collect.Lists; @@ -3779,192 +3779,210 @@ public void disallowDuplicateAliasesAcrossStages() { @Test public void testInsertWithIdExpression() throws Exception { - CollectionReference targetCol = firestore.collection(LocalFirestoreHelper.autoId()); + CollectionReference targetCol = firestore.collection(LocalFirestoreHelper.autoId()); - java.util.Map book = new java.util.HashMap<>(); - book.put("title", "The Hitchhiker's Guide to the Galaxy"); - book.put("doc_id", "book1"); + java.util.Map book = new java.util.HashMap<>(); + book.put("title", "The Hitchhiker's Guide to the Galaxy"); + book.put("doc_id", "book1"); + if ("NIGHTLY".equals(getTargetBackend())) { firestore - .pipeline() - .literals(book) - .insert(new Insert().withCollection(targetCol).withIdExpression(field("doc_id"))) - .execute() - .get(); + .pipeline() + .literals(book) + .insert(new Insert().withCollection(targetCol).withIdExpression(field("doc_id"))) + .execute() + .get(); DocumentSnapshot doc = targetCol.document("book1").get().get(); assertThat(doc.exists()).isTrue(); assertThat(doc.get("title")).isEqualTo("The Hitchhiker's Guide to the Galaxy"); + } else { + assertThrows( + ExecutionException.class, + () -> { + firestore + .pipeline() + .literals(book) + .insert(new Insert().withCollection(targetCol).withIdExpression(field("doc_id"))) + .execute() + .get(); + }); + } } @Test public void testInsertToExistingDocumentFails() throws Exception { - CollectionReference targetCol = firestore.collection(LocalFirestoreHelper.autoId()); - targetCol.document("book1").set(map("title", "Existing")).get(); + CollectionReference targetCol = firestore.collection(LocalFirestoreHelper.autoId()); + targetCol.document("book1").set(map("title", "Existing")).get(); - java.util.Map book = new java.util.HashMap<>(); - book.put("title", "Duplicate"); - book.put("__name__", targetCol.document("book1")); + java.util.Map book = new java.util.HashMap<>(); + book.put("title", "Duplicate"); + book.put("__name__", targetCol.document("book1")); - assertThrows( - ExecutionException.class, - () -> { - firestore - .pipeline() - .literals(book) - .insert(targetCol) - .execute() - .get(); - }); + assertThrows( + ExecutionException.class, + () -> { + firestore.pipeline().literals(book).insert(targetCol).execute().get(); + }); } @Test public void testDeleteMultipleDocuments() throws Exception { CollectionReference dmlCol = testCollectionWithDocs(bookDocs); - List results = - firestore - .pipeline() - .collection(dmlCol.getPath()) - .where(equal(field("genre"), "Science Fiction")) - .delete() - .execute() - .get() - .getResults(); - - assertThat(results).hasSize(1); - assertThat(results.get(0).getData().get("documents_modified")).isEqualTo(2L); - assertThat(dmlCol.document("book1").get().get().exists()).isFalse(); - assertThat(dmlCol.document("book10").get().get().exists()).isFalse(); + if ("NIGHTLY".equals(getTargetBackend())) { + List results = + firestore + .pipeline() + .collection(dmlCol.getPath()) + .where(equal(field("genre"), "Science Fiction")) + .delete() + .execute() + .get() + .getResults(); + + assertThat(results).hasSize(1); + assertThat(results.get(0).getData().get("documents_modified")).isEqualTo(2L); + assertThat(dmlCol.document("book1").get().get().exists()).isFalse(); + assertThat(dmlCol.document("book10").get().get().exists()).isFalse(); + } else { + assertThrows( + ExecutionException.class, + () -> { + firestore + .pipeline() + .collection(dmlCol.getPath()) + .where(equal(field("genre"), "Science Fiction")) + .delete() + .execute() + .get(); + }); + } } @Test public void testUpdateMultipleDocuments() throws Exception { CollectionReference dmlCol = testCollectionWithDocs(bookDocs); - List results = - firestore - .pipeline() - .collection(dmlCol.getPath()) - .where(equal(field("genre"), "Science Fiction")) - .update(constant("Updated").as("status")) - .execute() - .get() - .getResults(); + if ("NIGHTLY".equals(getTargetBackend())) { + List results = + firestore + .pipeline() + .collection(dmlCol.getPath()) + .where(equal(field("genre"), "Science Fiction")) + .removeFields("awards") + .update(constant("Updated").as("status")) + .execute() + .get() + .getResults(); - assertThat(results).hasSize(1); - assertThat(results.get(0).getData().get("documents_modified")).isEqualTo(2L); - assertThat(dmlCol.document("book1").get().get().get("status")).isEqualTo("Updated"); - assertThat(dmlCol.document("book10").get().get().get("status")).isEqualTo("Updated"); + assertThat(results).hasSize(1); + assertThat(results.get(0).getData().get("documents_modified")).isEqualTo(2L); + assertThat(dmlCol.document("book1").get().get().get("status")).isEqualTo("Updated"); + assertThat(dmlCol.document("book1").get().get().get("awards")).isNull(); + + assertThat(dmlCol.document("book10").get().get().get("status")).isEqualTo("Updated"); + assertThat(dmlCol.document("book10").get().get().get("awards")).isNull(); + } else { + assertThrows( + ExecutionException.class, + () -> { + firestore + .pipeline() + .collection(dmlCol.getPath()) + .where(equal(field("genre"), "Science Fiction")) + .removeFields("awards") + .update(constant("Updated").as("status")) + .execute() + .get(); + }); + } } @Test public void testUpdateWithExpressions() throws Exception { CollectionReference dmlCol = testCollectionWithDocs(bookDocs); - List results = - firestore - .pipeline() - .collection(dmlCol.getPath()) - .where(equal(field("__name__").documentId(), "book1")) - .update(Expression.add(field("rating"), constant(1.0)).as("rating")) - .execute() - .get() - .getResults(); + if ("NIGHTLY".equals(getTargetBackend())) { + List results = + firestore + .pipeline() + .collection(dmlCol.getPath()) + .where(equal(field("__name__").documentId(), "book1")) + .update( + com.google.cloud.firestore.pipeline.expressions.Expression.add( + field("rating"), constant(1.0)) + .as("rating")) + .execute() + .get() + .getResults(); - assertThat(results).hasSize(1); - DocumentSnapshot doc = dmlCol.document("book1").get().get(); - assertThat(doc.get("rating")).isEqualTo(5.2); -} + assertThat(results).hasSize(1); + DocumentSnapshot doc = dmlCol.document("book1").get().get(); + assertThat(doc.get("rating")).isEqualTo(5.2); + } else { + assertThrows( + ExecutionException.class, + () -> { + firestore + .pipeline() + .collection(dmlCol.getPath()) + .where(equal(field("__name__").documentId(), "book1")) + .update( + com.google.cloud.firestore.pipeline.expressions.Expression.add( + field("rating"), constant(1.0)) + .as("rating")) + .execute() + .get(); + }); + } + } -@Test -public void testUpdateNonExistingDocumentModifiesZeroDocuments() throws Exception { + @Test + public void testUpdateNonExistingDocumentModifiesZeroDocuments() throws Exception { CollectionReference dmlCol = firestore.collection(LocalFirestoreHelper.autoId()); java.util.Map book = new java.util.HashMap<>(); book.put("title", "Non Existing"); book.put("__name__", dmlCol.document("nonExisting")); - List results = - firestore - .pipeline() - .literals(book) - .update() - .execute() - .get() - .getResults(); - - assertThat(results).hasSize(1); - assertThat(results.get(0).getData().get("documents_modified")).isEqualTo(0L); -} - -@Test -public void testUpsertInPlaceReadingFromCollection() throws Exception { - assumeFalse( - "Transactions are not supported against the emulator.", - isRunningAgainstFirestoreEmulator(firestore)); - CollectionReference dmlCol = testCollectionWithDocs(bookDocs); - - firestore.runTransaction( - transaction -> { - transaction.execute( - firestore - .pipeline() - .collection(dmlCol.getPath()) - .where(equal(field("__name__").documentId(), "book3")) - .upsert(new Upsert().withTransformations(constant("Updated").as("status"))) - ).get(); - return "done"; - } - ).get(); - - DocumentSnapshot doc = dmlCol.document("book3").get().get(); - assertThat(doc.get("status")).isEqualTo("Updated"); -} - -@Test -public void testUpsertToNewCollectionReadingFromCollection() throws Exception { - assumeFalse( - "Transactions are not supported against the emulator.", - isRunningAgainstFirestoreEmulator(firestore)); - CollectionReference dmlCol = testCollectionWithDocs(bookDocs); - CollectionReference targetCol = firestore.collection(LocalFirestoreHelper.autoId()); - - firestore.runTransaction( - transaction -> { - transaction.execute( - firestore - .pipeline() - .collection(dmlCol.getPath()) - .where(equal(field("__name__").documentId(), "book1")) - .upsert(new Upsert().withCollection(targetCol).withTransformations(constant("Copied").as("status"))) - ).get(); - return "done"; - } - ).get(); - - DocumentSnapshot doc = targetCol.document("book1").get().get(); - assertThat(doc.exists()).isTrue(); - assertThat(doc.get("status")).isEqualTo("Copied"); - assertThat(dmlCol.document("book1").get().get().exists()).isTrue(); -} - + if ("NIGHTLY".equals(getTargetBackend())) { + List results = + firestore.pipeline().literals(book).update().execute().get().getResults(); + assertThat(results).hasSize(1); + assertThat(results.get(0).getData().get("documents_modified")).isEqualTo(0L); + } else { + assertThrows( + ExecutionException.class, + () -> { + firestore.pipeline().literals(book).update().execute().get(); + }); + } + } -@Test -public void testLiteralsStage() throws Exception { + @Test + public void testLiteralsStage() throws Exception { java.util.Map data1 = new java.util.HashMap<>(); data1.put("foo", "bar"); java.util.Map data2 = new java.util.HashMap<>(); data2.put("baz", "qux"); - List results = firestore - .pipeline() - .literals(data1, data2) - .execute() - .get() - .getResults(); + List results = + firestore.pipeline().literals(data1, data2).execute().get().getResults(); assertThat(results).hasSize(2); assertThat(results.get(0).getData()).isEqualTo(data1); assertThat(results.get(1).getData()).isEqualTo(data2); } -} + @Test + public void testLiteralsWithExpressions() throws Exception { + java.util.Map data = new java.util.HashMap<>(); + data.put("base", 10); + data.put("doubled", multiply(constant(10), constant(2))); + + List results = firestore.pipeline().literals(data).execute().get().getResults(); + + assertThat(results).hasSize(1); + assertThat(results.get(0).getData().get("base")).isEqualTo(10L); + assertThat(results.get(0).getData().get("doubled")).isEqualTo(20L); + } +} From a5bd4c9658b6e11db0f695f24fa7a31854bf35e0 Mon Sep 17 00:00:00 2001 From: wu-hui Date: Fri, 27 Mar 2026 11:07:37 -0400 Subject: [PATCH 6/6] Delete insert() --- .../com/google/cloud/firestore/Pipeline.java | 100 ++---------------- .../pipeline/stages/ConflictResolution.java | 35 ------ .../pipeline/stages/DeleteReturn.java | 36 ------- .../firestore/pipeline/stages/Insert.java | 55 ---------- .../pipeline/stages/InsertReturn.java | 36 ------- .../firestore/pipeline/stages/Update.java | 14 +-- .../pipeline/stages/UpdateReturn.java | 36 ------- .../firestore/pipeline/stages/Upsert.java | 82 -------------- .../pipeline/stages/UpsertReturn.java | 36 ------- .../cloud/firestore/it/ITPipelineTest.java | 50 --------- .../pipeline/stages/LiteralsTest.java | 79 -------------- 11 files changed, 13 insertions(+), 546 deletions(-) delete mode 100644 google-cloud-firestore/src/main/java/com/google/cloud/firestore/pipeline/stages/ConflictResolution.java delete mode 100644 google-cloud-firestore/src/main/java/com/google/cloud/firestore/pipeline/stages/DeleteReturn.java delete mode 100644 google-cloud-firestore/src/main/java/com/google/cloud/firestore/pipeline/stages/Insert.java delete mode 100644 google-cloud-firestore/src/main/java/com/google/cloud/firestore/pipeline/stages/InsertReturn.java delete mode 100644 google-cloud-firestore/src/main/java/com/google/cloud/firestore/pipeline/stages/UpdateReturn.java delete mode 100644 google-cloud-firestore/src/main/java/com/google/cloud/firestore/pipeline/stages/Upsert.java delete mode 100644 google-cloud-firestore/src/main/java/com/google/cloud/firestore/pipeline/stages/UpsertReturn.java delete mode 100644 google-cloud-firestore/src/test/java/com/google/cloud/firestore/pipeline/stages/LiteralsTest.java diff --git a/google-cloud-firestore/src/main/java/com/google/cloud/firestore/Pipeline.java b/google-cloud-firestore/src/main/java/com/google/cloud/firestore/Pipeline.java index bd8683868..5970192d6 100644 --- a/google-cloud-firestore/src/main/java/com/google/cloud/firestore/Pipeline.java +++ b/google-cloud-firestore/src/main/java/com/google/cloud/firestore/Pipeline.java @@ -45,7 +45,6 @@ import com.google.cloud.firestore.pipeline.stages.Distinct; import com.google.cloud.firestore.pipeline.stages.FindNearest; import com.google.cloud.firestore.pipeline.stages.FindNearestOptions; -import com.google.cloud.firestore.pipeline.stages.Insert; import com.google.cloud.firestore.pipeline.stages.Limit; import com.google.cloud.firestore.pipeline.stages.Offset; import com.google.cloud.firestore.pipeline.stages.PipelineExecuteOptions; @@ -1075,12 +1074,12 @@ public Pipeline update() { * .get(); * } * - * @param transformations The transformations to apply. + * @param transformedFields The transformations to apply. * @return A new {@code Pipeline} object with this stage appended to the stage list. */ @BetaApi - public Pipeline update(Selectable... transformations) { - return append(new Update().withTransformations(transformations)); + public Pipeline update(Selectable... transformedFields) { + return append(new Update().withTransformedFields(transformedFields)); } /** @@ -1091,7 +1090,7 @@ public Pipeline update(Selectable... transformations) { *

Example: * *

{@code
-   * Update updateStage = new Update().withTransformations(constant("Updated").as("status"));
+   * Update updateStage = new Update().withTransformedFields(constant("Updated").as("status"));
    *
    * firestore.pipeline()
    *     .collection("books")
@@ -1110,95 +1109,8 @@ public Pipeline update(Update update) {
   }
 
   /**
-   * Performs an insert operation using documents from previous stages.
-   *
-   * 

The documents must include a valid {@code __name__} field specifying the document reference - * to insert. If the document already exists, the operation will fail. - * - *

Example: - * - *

{@code
-   * Map book = new HashMap<>();
-   * book.put("__name__", firestore.collection("books").document("newBook"));
-   * book.put("title", "New Book");
-   *
-   * firestore.pipeline()
-   *     .literals(book)
-   *     .insert()
-   *     .execute()
-   *     .get();
-   * }
- * - * @return A new {@code Pipeline} object with this stage appended to the stage list. - */ - @BetaApi - public Pipeline insert() { - return append(new Insert()); - } - - /** - * Performs an insert operation using documents from previous stages into a specified target - * collection. - * - *

If documents have an ID (or expression evaluation for ID), they will use it. Otherwise, - * auto-generated IDs will be used if applicable (depending on the source). - * - *

Example: - * - *

{@code
-   * CollectionReference backupCol = firestore.collection("books_backup");
-   *
-   * Map book = new HashMap<>();
-   * book.put("title", "New Book");
-   *
-   * firestore.pipeline()
-   *     .literals(book)
-   *     .insert(backupCol)
-   *     .execute()
-   *     .get();
-   * }
- * - * @param target The collection to insert to. - * @return A new {@code Pipeline} object with this stage appended to the stage list. - */ - @BetaApi - public Pipeline insert(CollectionReference target) { - return append(new Insert().withCollection(target)); - } - - /** - * Performs an insert operation using an {@link Insert} stage. - * - *

This method allows you to use a pre-configured {@link Insert} stage. - * - *

Example: Use a pre-configured {@link Insert} stage with a target collection and ID - * expression, reading from literals. - * - *

{@code
-   * CollectionReference targetCol = firestore.collection("books_backup");
-   * Insert insertStage = new Insert().withCollection(targetCol).withIdExpression(field("custom_id"));
-   *
-   * Map book = new HashMap<>();
-   * book.put("custom_id", "book1");
-   * book.put("title", "Book 1");
-   *
-   * firestore.pipeline()
-   *     .literals(book)
-   *     .insert(insertStage)
-   *     .execute()
-   *     .get();
-   * }
- * - * @param insertStage The {@code Insert} stage to append. - * @return A new {@code Pipeline} object with this stage appended to the stage list. - */ - @BetaApi - public Pipeline insert(Insert insertStage) { - return append(insertStage); - } - - /** - * Adds a generic stage to the pipeline. + * Performs an insert operation using documents from previous stages. Adds a generic stage to the + * pipeline. * *

This method provides a flexible way to extend the pipeline's functionality by adding custom * stages. Each generic stage is defined by a unique `name` and a set of `params` that control its diff --git a/google-cloud-firestore/src/main/java/com/google/cloud/firestore/pipeline/stages/ConflictResolution.java b/google-cloud-firestore/src/main/java/com/google/cloud/firestore/pipeline/stages/ConflictResolution.java deleted file mode 100644 index 8766b7c05..000000000 --- a/google-cloud-firestore/src/main/java/com/google/cloud/firestore/pipeline/stages/ConflictResolution.java +++ /dev/null @@ -1,35 +0,0 @@ -/* - * 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.pipeline.stages; - -/** Defines the conflict resolution options for an Upsert pipeline stage. */ -public enum ConflictResolution { - OVERWRITE("OVERWRITE"), - MERGE("MERGE"), - FAIL("FAIL"), - KEEP("KEEP"); - - private final String value; - - ConflictResolution(String value) { - this.value = value; - } - - public String getValue() { - return value; - } -} diff --git a/google-cloud-firestore/src/main/java/com/google/cloud/firestore/pipeline/stages/DeleteReturn.java b/google-cloud-firestore/src/main/java/com/google/cloud/firestore/pipeline/stages/DeleteReturn.java deleted file mode 100644 index ff81f44e8..000000000 --- a/google-cloud-firestore/src/main/java/com/google/cloud/firestore/pipeline/stages/DeleteReturn.java +++ /dev/null @@ -1,36 +0,0 @@ -/* - * 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.pipeline.stages; - -import com.google.api.core.InternalApi; - -/** Defines the return value options for a Delete pipeline stage. */ -@InternalApi -public enum DeleteReturn { - EMPTY("EMPTY"), - DOCUMENT_ID("DOCUMENT_ID"); - - private final String value; - - DeleteReturn(String value) { - this.value = value; - } - - public String getValue() { - return value; - } -} diff --git a/google-cloud-firestore/src/main/java/com/google/cloud/firestore/pipeline/stages/Insert.java b/google-cloud-firestore/src/main/java/com/google/cloud/firestore/pipeline/stages/Insert.java deleted file mode 100644 index 21df157e8..000000000 --- a/google-cloud-firestore/src/main/java/com/google/cloud/firestore/pipeline/stages/Insert.java +++ /dev/null @@ -1,55 +0,0 @@ -/* - * 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.pipeline.stages; - -import com.google.api.core.BetaApi; -import com.google.cloud.firestore.CollectionReference; -import com.google.cloud.firestore.PipelineUtils; -import com.google.cloud.firestore.pipeline.expressions.Expression; -import com.google.firestore.v1.Value; - -@BetaApi -public final class Insert extends Stage { - - private Insert(InternalOptions options) { - super("insert", options); - } - - @BetaApi - public Insert() { - this(InternalOptions.EMPTY); - } - - @BetaApi - public Insert withCollection(CollectionReference target) { - String path = target.getPath(); - String normalizedPath = path.startsWith("/") ? path : "/" + path; - return new Insert( - this.options.with( - "collection", Value.newBuilder().setReferenceValue(normalizedPath).build())); - } - - @BetaApi - public Insert withIdExpression(Expression idExpr) { - return new Insert(this.options.with("document_id", PipelineUtils.encodeValue(idExpr))); - } - - @Override - Iterable toStageArgs() { - return java.util.Collections.emptyList(); - } -} diff --git a/google-cloud-firestore/src/main/java/com/google/cloud/firestore/pipeline/stages/InsertReturn.java b/google-cloud-firestore/src/main/java/com/google/cloud/firestore/pipeline/stages/InsertReturn.java deleted file mode 100644 index a0dac9ade..000000000 --- a/google-cloud-firestore/src/main/java/com/google/cloud/firestore/pipeline/stages/InsertReturn.java +++ /dev/null @@ -1,36 +0,0 @@ -/* - * 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.pipeline.stages; - -import com.google.api.core.InternalApi; - -/** Defines the return value options for an Insert pipeline stage. */ -@InternalApi -public enum InsertReturn { - EMPTY("EMPTY"), - DOCUMENT_ID("DOCUMENT_ID"); - - private final String value; - - InsertReturn(String value) { - this.value = value; - } - - public String getValue() { - return value; - } -} diff --git a/google-cloud-firestore/src/main/java/com/google/cloud/firestore/pipeline/stages/Update.java b/google-cloud-firestore/src/main/java/com/google/cloud/firestore/pipeline/stages/Update.java index 6e125026c..22d029045 100644 --- a/google-cloud-firestore/src/main/java/com/google/cloud/firestore/pipeline/stages/Update.java +++ b/google-cloud-firestore/src/main/java/com/google/cloud/firestore/pipeline/stages/Update.java @@ -31,11 +31,11 @@ @InternalApi public final class Update extends Stage { - @Nullable private final Selectable[] transformations; + @Nullable private final Selectable[] transformedFields; - private Update(@Nullable Selectable[] transformations, InternalOptions options) { + private Update(@Nullable Selectable[] transformedFields, InternalOptions options) { super("update", options); - this.transformations = transformations; + this.transformedFields = transformedFields; } @BetaApi @@ -44,15 +44,15 @@ public Update() { } @BetaApi - public Update withTransformations(Selectable... transformations) { - return new Update(transformations, this.options); + public Update withTransformedFields(Selectable... transformedFields) { + return new Update(transformedFields, this.options); } @Override Iterable toStageArgs() { List args = new ArrayList<>(); - if (transformations != null && transformations.length > 0) { - Map map = PipelineUtils.selectablesToMap(transformations); + if (transformedFields != null && transformedFields.length > 0) { + Map map = PipelineUtils.selectablesToMap(transformedFields); Map encodedMap = new HashMap<>(); for (Map.Entry entry : map.entrySet()) { encodedMap.put(entry.getKey(), PipelineUtils.encodeValue(entry.getValue())); diff --git a/google-cloud-firestore/src/main/java/com/google/cloud/firestore/pipeline/stages/UpdateReturn.java b/google-cloud-firestore/src/main/java/com/google/cloud/firestore/pipeline/stages/UpdateReturn.java deleted file mode 100644 index f70650a6b..000000000 --- a/google-cloud-firestore/src/main/java/com/google/cloud/firestore/pipeline/stages/UpdateReturn.java +++ /dev/null @@ -1,36 +0,0 @@ -/* - * 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.pipeline.stages; - -import com.google.api.core.InternalApi; - -/** Defines the return value options for an Update pipeline stage. */ -@InternalApi -public enum UpdateReturn { - EMPTY("EMPTY"), - DOCUMENT_ID("DOCUMENT_ID"); - - private final String value; - - UpdateReturn(String value) { - this.value = value; - } - - public String getValue() { - return value; - } -} diff --git a/google-cloud-firestore/src/main/java/com/google/cloud/firestore/pipeline/stages/Upsert.java b/google-cloud-firestore/src/main/java/com/google/cloud/firestore/pipeline/stages/Upsert.java deleted file mode 100644 index f1f646ee4..000000000 --- a/google-cloud-firestore/src/main/java/com/google/cloud/firestore/pipeline/stages/Upsert.java +++ /dev/null @@ -1,82 +0,0 @@ -/* - * Copyright 2024 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.google.cloud.firestore.pipeline.stages; - -import com.google.api.core.BetaApi; -import com.google.cloud.firestore.CollectionReference; -import com.google.cloud.firestore.PipelineUtils; -import com.google.cloud.firestore.pipeline.expressions.Expression; -import com.google.cloud.firestore.pipeline.expressions.Selectable; -import com.google.firestore.v1.Value; -import java.util.ArrayList; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import javax.annotation.Nullable; - -@BetaApi -public final class Upsert extends Stage { - - @Nullable private final Selectable[] transformations; - - private Upsert(@Nullable Selectable[] transformations, InternalOptions options) { - super("upsert", options); - this.transformations = transformations; - } - - @BetaApi - public Upsert() { - this(null, InternalOptions.EMPTY); - } - - @BetaApi - public Upsert withCollection(CollectionReference target) { - String path = target.getPath(); - String normalizedPath = path.startsWith("/") ? path : "/" + path; - return new Upsert( - this.transformations, - this.options.with( - "collection", Value.newBuilder().setReferenceValue(normalizedPath).build())); - } - - @BetaApi - public Upsert withIdExpression(Expression idExpr) { - return new Upsert( - this.transformations, this.options.with("document_id", PipelineUtils.encodeValue(idExpr))); - } - - @BetaApi - public Upsert withTransformations(Selectable... transformations) { - return new Upsert(transformations, this.options); - } - - @Override - Iterable toStageArgs() { - List args = new ArrayList<>(); - if (transformations != null && transformations.length > 0) { - Map map = PipelineUtils.selectablesToMap(transformations); - Map encodedMap = new HashMap<>(); - for (Map.Entry entry : map.entrySet()) { - encodedMap.put(entry.getKey(), PipelineUtils.encodeValue(entry.getValue())); - } - args.add(PipelineUtils.encodeValue(encodedMap)); - } else { - args.add(PipelineUtils.encodeValue(new HashMap())); - } - return args; - } -} diff --git a/google-cloud-firestore/src/main/java/com/google/cloud/firestore/pipeline/stages/UpsertReturn.java b/google-cloud-firestore/src/main/java/com/google/cloud/firestore/pipeline/stages/UpsertReturn.java deleted file mode 100644 index 7f799f534..000000000 --- a/google-cloud-firestore/src/main/java/com/google/cloud/firestore/pipeline/stages/UpsertReturn.java +++ /dev/null @@ -1,36 +0,0 @@ -/* - * 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.pipeline.stages; - -import com.google.api.core.InternalApi; - -/** Defines the return value options for an Upsert pipeline stage. */ -@InternalApi -public enum UpsertReturn { - EMPTY("EMPTY"), - DOCUMENT_ID("DOCUMENT_ID"); - - private final String value; - - UpsertReturn(String value) { - this.value = value; - } - - public String getValue() { - return value; - } -} diff --git a/google-cloud-firestore/src/test/java/com/google/cloud/firestore/it/ITPipelineTest.java b/google-cloud-firestore/src/test/java/com/google/cloud/firestore/it/ITPipelineTest.java index 20ade73d6..091b7cf92 100644 --- a/google-cloud-firestore/src/test/java/com/google/cloud/firestore/it/ITPipelineTest.java +++ b/google-cloud-firestore/src/test/java/com/google/cloud/firestore/it/ITPipelineTest.java @@ -121,7 +121,6 @@ import com.google.cloud.firestore.pipeline.stages.ExplainOptions; import com.google.cloud.firestore.pipeline.stages.FindNearest; import com.google.cloud.firestore.pipeline.stages.FindNearestOptions; -import com.google.cloud.firestore.pipeline.stages.Insert; import com.google.cloud.firestore.pipeline.stages.PipelineExecuteOptions; import com.google.cloud.firestore.pipeline.stages.RawOptions; import com.google.cloud.firestore.pipeline.stages.RawStage; @@ -3777,55 +3776,6 @@ public void disallowDuplicateAliasesAcrossStages() { assertThat(exception).hasMessageThat().contains("Duplicate alias or field name"); } - @Test - public void testInsertWithIdExpression() throws Exception { - CollectionReference targetCol = firestore.collection(LocalFirestoreHelper.autoId()); - - java.util.Map book = new java.util.HashMap<>(); - book.put("title", "The Hitchhiker's Guide to the Galaxy"); - book.put("doc_id", "book1"); - - if ("NIGHTLY".equals(getTargetBackend())) { - firestore - .pipeline() - .literals(book) - .insert(new Insert().withCollection(targetCol).withIdExpression(field("doc_id"))) - .execute() - .get(); - - DocumentSnapshot doc = targetCol.document("book1").get().get(); - assertThat(doc.exists()).isTrue(); - assertThat(doc.get("title")).isEqualTo("The Hitchhiker's Guide to the Galaxy"); - } else { - assertThrows( - ExecutionException.class, - () -> { - firestore - .pipeline() - .literals(book) - .insert(new Insert().withCollection(targetCol).withIdExpression(field("doc_id"))) - .execute() - .get(); - }); - } - } - - @Test - public void testInsertToExistingDocumentFails() throws Exception { - CollectionReference targetCol = firestore.collection(LocalFirestoreHelper.autoId()); - targetCol.document("book1").set(map("title", "Existing")).get(); - - java.util.Map book = new java.util.HashMap<>(); - book.put("title", "Duplicate"); - book.put("__name__", targetCol.document("book1")); - - assertThrows( - ExecutionException.class, - () -> { - firestore.pipeline().literals(book).insert(targetCol).execute().get(); - }); - } - @Test public void testDeleteMultipleDocuments() throws Exception { CollectionReference dmlCol = testCollectionWithDocs(bookDocs); diff --git a/google-cloud-firestore/src/test/java/com/google/cloud/firestore/pipeline/stages/LiteralsTest.java b/google-cloud-firestore/src/test/java/com/google/cloud/firestore/pipeline/stages/LiteralsTest.java deleted file mode 100644 index affa30fa0..000000000 --- a/google-cloud-firestore/src/test/java/com/google/cloud/firestore/pipeline/stages/LiteralsTest.java +++ /dev/null @@ -1,79 +0,0 @@ -/* - * 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.pipeline.stages; - -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertTrue; - -import com.google.cloud.firestore.pipeline.expressions.Field; -import com.google.firestore.v1.Value; -import java.util.HashMap; -import java.util.Map; -import org.junit.Test; - -public class LiteralsTest { - - @Test - public void testToStageArgsWithConstant() { - Map map = new HashMap<>(); - map.put("title", "The Hitchhiker's Guide to the Galaxy"); - - Literals literals = new Literals(map); - Iterable args = literals.toStageArgs(); - - assertTrue(args.iterator().hasNext()); - Value value = args.iterator().next(); - assertTrue(value.hasMapValue()); - assertEquals( - "The Hitchhiker's Guide to the Galaxy", - value.getMapValue().getFieldsOrThrow("title").getStringValue()); - } - - @Test - public void testToStageArgsWithExpression() { - Map map = new HashMap<>(); - map.put("title", "The Hitchhiker's Guide to the Galaxy"); - map.put( - "message", - com.google.cloud.firestore.pipeline.expressions.Expression.concat( - Field.ofServerPath("user_name"), " is awesome")); - - Literals literals = new Literals(map); - Iterable args = literals.toStageArgs(); - - assertTrue(args.iterator().hasNext()); - Value value = args.iterator().next(); - assertTrue(value.hasMapValue()); - assertEquals( - "The Hitchhiker's Guide to the Galaxy", - value.getMapValue().getFieldsOrThrow("title").getStringValue()); - - Value messageValue = value.getMapValue().getFieldsOrThrow("message"); - assertTrue(messageValue.hasFunctionValue()); - com.google.firestore.v1.Function func = messageValue.getFunctionValue(); - assertEquals("concat", func.getName()); - assertEquals(2, func.getArgsCount()); - - Value arg0 = func.getArgs(0); - assertTrue(arg0.hasFieldReferenceValue()); - assertEquals("user_name", arg0.getFieldReferenceValue()); - - Value arg1 = func.getArgs(1); - assertTrue(arg1.hasStringValue()); - assertEquals(" is awesome", arg1.getStringValue()); - } -}