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! 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..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 @@ -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,10 @@ 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; import com.google.cloud.firestore.telemetry.MetricsUtil.MetricsContext; import com.google.cloud.firestore.telemetry.TelemetryConstants; @@ -995,6 +1003,138 @@ 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. + */ + @InternalApi + 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. + */ + @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. + * + * @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. + */ + @InternalApi + 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..b72e89e86 --- /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); + } + + @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()))); + } + + @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..356212920 --- /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.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/DeleteReturn.java b/google-cloud-firestore/src/main/java/com/google/cloud/firestore/pipeline/stages/DeleteReturn.java new file mode 100644 index 000000000..ff81f44e8 --- /dev/null +++ b/google-cloud-firestore/src/main/java/com/google/cloud/firestore/pipeline/stages/DeleteReturn.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 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 new file mode 100644 index 000000000..98723cd28 --- /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); + } + + @InternalApi + public Insert withOptions(InsertOptions options) { + return new Insert(path, this.options.adding(options)); + } + + @InternalApi + 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..cb6afc038 --- /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.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/InsertReturn.java b/google-cloud-firestore/src/main/java/com/google/cloud/firestore/pipeline/stages/InsertReturn.java new file mode 100644 index 000000000..a0dac9ade --- /dev/null +++ b/google-cloud-firestore/src/main/java/com/google/cloud/firestore/pipeline/stages/InsertReturn.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 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 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 new file mode 100644 index 000000000..e4d33c875 --- /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); + } + + @InternalApi + public Upsert withOptions(UpsertOptions options) { + return new Upsert(path, this.options.adding(options)); + } + + @InternalApi + 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..e71b8a7fb --- /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.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/UpsertReturn.java b/google-cloud-firestore/src/main/java/com/google/cloud/firestore/pipeline/stages/UpsertReturn.java new file mode 100644 index 000000000..7f799f534 --- /dev/null +++ b/google-cloud-firestore/src/main/java/com/google/cloud/firestore/pipeline/stages/UpsertReturn.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 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/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..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 @@ -3773,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(); + } }